多线程
关于多线程有关的概念:
- 进程:进程指正在运行的程序,并且具有一定独立功能
- 线程:线程是进程中的一个执行单位,负责当前进程程序的执行,一个进程中至少会有一个线程,如果一个进程中包含多个线程,那么可称为多线程程序
- 单线程:当要执行多个任务时,cpu只会依次执行,当一个任务执行完后,再去执行另外一个任务
- 多线程:多个任务可以同时进行
在Java中,不同线程会有不同的优先级抢占cpu,如果线程优先级相同,就会随机先去一个线程去执行
Java程序运行时会默认执行3个进程:
- main主线程
- gc垃圾回收机制
- 异常处理机制
我们如何能够判断程序是否是多线程?
如果我们能够将程序的执行用一条直线画出来,就说明是单线程
关于线程的常用API方法
- run():该方法需要被重写,重写的内容就是需要执行的操作
- start():调用该方法就会启动相应的线程,并调用当前线程的run方法
- sleep(long millitime):将当前线程进入阻塞状态(不会释放锁,即同步监视器)
- join():当a线程调用b线程的join方法时,a线程会进入阻塞状态,直到b线程的任务执行完毕
- isAlive():判断当前线程是否存活
- yield():调用该方法后回释放当前线程的cpu执行权,当时并不代表不会再次执行,有可能释放后,又是该线程抢占到了cpu的执行权
- currentThread():Thread类中的静态方法,会返回执行当前程序的线程
- getName():返回当前线程的名字
- setName():设置线程的名字
- getPriority():设置线程的优先级(
MAX_PRIORITY=10
MIN_PRIORITY=1
NORM_PRIORITY=5 默认优先级
- wait():将线程进入阻塞状态(会释放掉锁),只能在同步代码块或同步方法中使用
- notify():将另外一个线程唤醒
- notifyAll():唤醒所有被阻塞的线程
线程创建的4种方式
一:继承Thread类
- 首先创建一个类去继承Thread
- 重写Thread中的run方法
- main()中创建该对象
- 调用该对象的start方法,启动线程
public class test { public static void main(String[] args) { MyThread1 myThread1=new MyThread1(); myThread1.start(); } } class MyThread1 extends Thread{ @Override public void run() { System.out.println("继承了Thread"); } }
二:实现Runnable接口
- 创建一个类实现Runnable接口
- 实现接口的run方法
- 在main()中创建实现Runnable的对象
- 创建Thread对象,并把刚创建好的类传参
- 调用Thread对象的start方法,启动线程
public class test { public static void main(String[] args) { MyThread1 myThread1=new MyThread1(); Thread t=new Thread(myThread1); t.start(); } } class MyThread1 implements Runnable{ @Override public void run() { System.out.println("实现了Runnable"); } }
三:实现Callable接口
- 创建一个类实现Callable接口
- 实现接口的call()方法
- 在main中创建实现Callable的对象
- 创建FutureTask对象并把上面创建的对象传参
- 创建Thread对象,传入FutureTask对象
- 调用Thread的start方法,启动线程
public class test { public static void main(String[] args) { MyThread2 myThread2=new MyThread2(); FutureTask futureTask=new FutureTask(myThread2); Thread t=new Thread(futureTask); t.start(); } } class MyThread2 implements Callable{ @Override public Object call() throws Exception { System.out.println("实现了Callable"); return null; } }
此方式如果需要得到返回值需要调用futureTask.get();
但是会抛异常,用try,catch方法捕捉一下就好了
四:线程池
- 创建ExecutorService对象
- 传入相应的线程对象
- 结束线程池
public class test { public static void main(String[] args) { //创建线程池,设置线程池线程的数量为10 ExecutorService service = Executors.newFixedThreadPool(10); //execute适用于实现了Runnable的对象 service.execute(new MyThread1()); //submit适用于实现了Callable的对象 service.submit(new MyThread2()); //结束线程池 service.shutdown(); } } class MyThread1 implements Runnable{ @Override public void run() { System.out.println("实现了Runnable"); } } class MyThread2 implements Callable{ @Override public Object call() throws Exception { System.out.println("实现了Callable"); return null; } }
Runnable和Callable的对比:
- Callable可以有返回值,执行完相应操作后可以返回需要的结果
- 可以抛异常,可以将call方法中的异常抛出
- 支持泛型,可以指定返回值类型
线程安全
什么是线程安全问题呢?
当多个线程对同一个共享数据操作时,线程执行还没来得及更新处理共享的数据,从而使得其他操作的线程并未得到最新的数据,从而产生问题
举个例子:
- 当甲乙两人向同一账户存钱,让甲乙两个线程同时存钱,如果甲向账户存了1000元,并打印此时余额,应为1000元,但是如果此时乙也存了1000元,就会导致,显示余额为2000元,并不是甲当时的余额
- 还有就是火车售票问题,如果多个窗口同时售票,如果1号窗口正在卖001号票时,此时还未处理完成,这是2号窗口也卖了001号票,这就导致产生了两个001号票
那么如何解决呢?
有三种方式:
方法一:同步代码块
synchronized(Object obj) { //操作内容 }
- synchronized():传入的可以是任意类的对象,但必须是多个线程共用的,一般可以利用this,即当前对象(Runnable),Thread不太行,因为继承多个Thread类会导致this对象不一致
- 被包住的代码执行为单线程,当一个线程执行完后,另外一个线程才有可能会分配到执行权去执行
- 多个线程必须共用同一把锁,这样才能够判断一个线程是够执行
- Runnable一般很实用,因为多个线程都调用同一个类的方法,但是Thread就需要自己定义静态变量或者当前的唯一类即(windows.class)
public class test { public static void main(String[] args) { Window t1 = new Window("窗口1"); Window t2 = new Window("窗口2"); Window t3 = new Window("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window extends Thread{ private static int ticket=100; //继承方式,要用静态对象,因为继承实现多线程有多个对象,不是共用一个对象 private static Object obj=new Object(); public Window(String name){ super(name); } @Override public void run() { while(true){ //不可以用this,同理,因为有很多对象,不唯一 synchronized(obj){ if(ticket>0){ System.out.println(getName()+":卖票,票号为:"+ticket); ticket--; }else{ break; } } } } }
方法二:同步方法
和同步代码块类似
就是将共享数据的操作封装成方法
将这个方法用锁锁住
public class test { public static void main(String[] args) { Window t1 = new Window("窗口1"); Window t2 = new Window("窗口2"); Window t3 = new Window("窗口3"); t1.start(); t2.start(); t3.start(); } } class Window extends Thread{ private static int ticket=100; public Window(String name){ super(name); } @Override public void run() { while(true){ show(); } } public static synchronized void show(){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket); ticket--; } } }
注意:
- show方法要定义成静态,因为如果是非静态,它的锁默认是当前对象,继承方式就会有多个锁,如果是Runnable就可以,所以需要编程静态,这样默认锁就是当前类对象
方法三:lock锁
- 首先创建一个ReentrantLock对象
- 在执行共享数据之前将锁打开,调用lock方法
- 在结束时将锁解开,调用unlock方法
public class test { public static void main(String[] args) { Windows w = new Windows(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class Windows implements Runnable{ private int ticket=100; private ReentrantLock lock=new ReentrantLock(true); @Override public void run() { while(true){ try { lock.lock(); if(ticket>0){ System.out.println(Thread.currentThread().getName()+":卖票,票号为:"+ticket); ticket--; } else{ break; } } finally { lock.unlock(); } } } }
问题:该方式和synchrnized有什么不同呢?
synchronized在执行完相应代码后会自动上锁解锁,而lock需要手动上锁和解锁,较为灵活
线程的死锁
描述:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
举个例子:两个人迎面相遇,甲希望乙会给他让路,而乙希望甲给他让他让路,就这样两个人僵持在这里,最终谁也不给谁让路,导致死锁问题
public class test { public static void main(String[] args) { StringBuffer s1=new StringBuffer(); StringBuffer s2=new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
本例中,线程1首先拿到了s1锁,然后阻塞了一段时间,这段时间线程2拿到了s2锁,这是线程1就绪需要s2锁,而线程2需要s1锁,两者谁都拿不到,就会僵持住
解决死锁的相应办法:
- 减少共享变量的使用
- 设计相应的算法去规避死锁问题
- 尽量减少锁的嵌套使用
线程的通信
- wait():将线程进入阻塞状态(会释放掉锁),只能在同步代码块或同步方法中使用
- notify():将另外一个优先级高的线程唤醒
- notifyAll():唤醒所有被阻塞的线程
注意:这三个方法只能够在同步代码块或者同步方法使用,都定义在了Object类中
例题要求:
让两个线程交替打印1-100之间的数字
public class 线程通信 { public static void main(String[] args) { Number number=new Number(); Thread t1=new Thread(number); Thread t2=new Thread(number); t1.setName("线程一"); t2.setName("线程二"); t1.start(); t2.start(); } } class Number implements Runnable{ private int number=1; @Override public void run() { while(true){ synchronized (this) { notify(); //唤醒全部 // notifyAll(); if(number<=100){ System.out.println(Thread.currentThread().getName()+":"+number); number++; try { //使得调用如下方法进程阻塞,执行wait后,锁就被释放 wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } }
- 当线程1首次进入,会打印出1,然后调用了wait方法 ,进入阻塞状态
- 此时线程2进入,首先会唤醒线程1,然后打印2,然后自己进入阻塞状态
- 两者交替阻塞唤醒,直到打印完为止
那么问题是sleep和wait方法有什么异同?
两个方法声明的位置不同,sleep是Thread中声明的,而wait是Object中声明的
sleep可以在任何情景调用,而wait只能够在同步代码块或同步方法中使用
sleep执行后不会释放当前的锁,而wait会释放掉当前的锁
生产者和消费者问题:
生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
package 生产者与消费者问题; public class 生产者 { public static void main(String[] args) { Clerk clerk=new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生产者"); Consumer c1 = new Consumer(clerk); c1.setName("消费者"); p1.start(); c1.start(); } } class Clerk{ private int productCount=0; public synchronized void consumeProduct() { if(productCount>0){ System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品"); productCount--; notify(); }else{ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void produceProduct() { if(productCount<20){ productCount++; System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品"); notify(); }else{ try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{ private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(getName()+":开始生产产品......"); while(true){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } clerk.produceProduct(); } } } class Consumer extends Thread{ private Clerk clerk; public Consumer(Clerk clerk){ this.clerk=clerk; } @Override public void run() { System.out.println(getName()+":开始消费产品......"); while(true){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } }
- 生产者和消费者会共享产品数量
- 我们可以在售货员类中定义方法,当此时的生产数量未达到标准时,就会进行生产,然后会唤醒消费的进程,否则就会进入阻塞状态
- 而当产品数量不足时,消费者就会唤醒生产进程,此时,自己进入阻塞状态