Java学习笔记---多线程并发

  • (一)认识线程和进程
  • (二)java中实现多线程的三种手段
    • 【1】在java中实现多线程操作有三种手段:
    • 【2】为什么更推荐使用Runnable接口?
    • 【3】【补充知识点】
    • 【4】继承Thread类案例(多线程实现类)
    • 【5】实现Runnable接口案例(==推荐使用==)
    • 【6】Thread类和Runnable接口的区别
    • 【7】实现Callable接口
  • (三)线程的状态
  • (四)线程操作的方法
    • 【1】方法简介
    • 【2】方法使用实例
      • (1)sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
      • (2)join():指等待t线程终止
      • (3)yield():暂停当前正在执行的线程对象,并执行其他线程
      • (4)setPriority(): 更改线程的优先级
      • (5)interrupt()
      • (6)wait()函数
    • 【3】线程操作方法总结
      • (1)wait和sleep区别
  • (五)同步与死锁
    • (5.1)使用同步解决
    • (5.3)认识关键字Synchronized和Volatile
      • (5.3.1)Volatile
      • (5.3.2)Synchronized说明
      • (5.3.3)Synchronized使用实例
      • (5.3.4)两者的区别
    • (5.4)使用死锁解决

(一)认识线程和进程

进程是线程一次动态执行过程,它需要经历从代码加载、代码执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。因为每个进程都能循环获得自己的CPU时间片,加上CPU执行速度非常快,使得所有程序好像是在同时运行一样。

线程是比进程更小的执行单位,多线程是指进程在执行过程中可以产生多个更小的程序单元,一个进程可以包含多个同时执行的线程。

一个进程是一个独立的运行环境,可以被看做一个程序或者一个应用。而线程是在进程中执行的一个任务。Java运行环境是一个包含了不同的类和程序的单一进程。线程可以被称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。例如你打开一个word文档,那就是开启了一个进程,而在这个进程纸上又有很多其他程序在运行,比如拼写检查,那么这些程序就是一个小的线程。

【进程】每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)

【线程】同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)

【多进程】是指操作系统能同时运行多个任务(程序)。

【多线程】是指在同一程序中有多个顺序流在执行。

(二)java中实现多线程的三种手段

【1】在java中实现多线程操作有三种手段:

1-继承Thread类
2-实现Runnable接口(推荐使用)
3-实现Callable接口(推荐使用)

【2】为什么更推荐使用Runnable接口?

1-如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
2-Runnable接口适合多个相同的程序代码的线程去处理同一个资源
3-Runnable接口可以避免java中的单继承的限制
4-Runnable接口增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
5-线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

【3】【补充知识点】

main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。

在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM实习在就是在操作系统中启动了一个进程。

【4】继承Thread类案例(多线程实现类)

先继承Thread多线程实现类,并且覆写Thread类中的run方法。

【使用继承的好处】
在run方法内获取当前线程直接使用this就可以了,不需要使用Thread.currentThread()方法

【使用继承的坏处】
(1)java不支持多继承,继承了Thread类就不能继承其他的类了
(2)另外任务和代码没有分离,当多个线程执行一样的任务时就需要多分代码,而实现Runable接口没有这种限制。

【Thread类案例一】
(1)多线程类

public class KillThread extends Thread{//私有类对象private Hero h1;private Hero h2;//公有构造器public KillThread(Hero h1, Hero h2){this.h1 = h1;this.h2 = h2;}//覆写run方法public void run(){while(!h2.isDead()){h1.attackHero(h2);}}
}

(2)测试类

public class TestThread {public static void main(String[] args) {Hero gareen = new Hero("盖伦",616,100);Hero teemo = new Hero("提莫",300,130);Hero bh = new Hero("赏金猎人",350,165);Hero leesin = new Hero("盲僧",455,180);//创建并且实例化多线程类KillThread,并且调用start方法,实现run方法KillThread killThread1 = new KillThread(gareen,teemo);killThread1.start();KillThread killThread2 = new KillThread(bh,leesin);killThread2.start();  }
}

(3)注意
1-在调用的时候不能直接调用run方法,应该是调用从Thread类中继承而来的start方法,实际上调用的还是run方法定义的主体。
2-两个线程对象是交错运行的,哪个线程对象抢到了CPU资源,哪个线程就可以运行,所以每次的运行结果是不一样的。
3-一个类的对象只能调用一次start方法,如果多次调用会抛出异常。

【Thread类案例二】

class Thread1 extends Thread{private String name;public Thread1(String name) {this.name=name;}public void run() {for (int i = 0; i < 5; i++) {System.out.println(name + "运行  :  " + i);try {sleep((int) Math.random() * 10);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class Main {public static void main(String[] args) {Thread1 mTh1=new Thread1("A");Thread1 mTh2=new Thread1("B");mTh1.start();mTh2.start();}}

输出:
A运行 : 0
B运行 : 0
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4

再运行一下:
A运行 : 0
B运行 : 0
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4

【说明】
1-程序启动运行main时候,java虚拟机启动一个进程,主线程main在main()调用时候被创建。随着调用MitiSay的两个对象的start方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。
2-start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。
3-start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。

【5】实现Runnable接口案例(推荐使用)

Runnable接口中只定义了一个抽象方法run

【Runnable接口案例一】
(1)多线程类

public class Battle implements Runnable{private Hero h1;private Hero h2;public Battle(Hero h1, Hero h2){this.h1 = h1;this.h2 = h2;}public void run(){while(!h2.isDead()){h1.attackHero(h2);}}
}

(2)测试类

public class TestThread {public static void main(String[] args) {Hero gareen = new Hero("盖伦",616,100);Hero teemo = new Hero("提莫",300,130);Hero bh = new Hero("赏金猎人",350,165);Hero leesin = new Hero("盲僧",455,180);Battle battle1 = new Battle(gareen,teemo);new Thread(battle1).start();Battle battle2 = new Battle(bh,leesin);new Thread(battle2).start();}
}

(3)注意
调用start的方法有所不同,是new Thread(battle1).start();,最终都必须依靠Thread类才能启动多线程。battle1是给线程指定一个名称。

【Runnable接口案例二】

class Thread2 implements Runnable{private String name;public Thread2(String name) {this.name=name;}@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println(name + "运行  :  " + i);try {Thread.sleep((int) Math.random() * 10);} catch (InterruptedException e) {e.printStackTrace();}}}}
public class Main {public static void main(String[] args) {new Thread(new Thread2("C")).start();new Thread(new Thread2("D")).start();}
}

【说明】
1-Thread2类通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
2-在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
3-实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

【6】Thread类和Runnable接口的区别

(1)区别一

public class Thread implements Runnable{public void run() {if (this.target != null) {this.target.run();}}
}

Thread类也是Runnable接口的子类,但是在Thread类中并没有完全实现Runnable接口中的run方法,直接调用Runnable接口中的run方法,也就是说这个方法是由Runnable的子类完成的

(2)区别二
如果是继承Thread类,则不适合多个线程共享资源,而实现了Runnable接口,可以方便的实现资源的共享。
例如卖票系统卖10张票,不共享资源的时候,每个线程分别卖10张票,一共30张票。而共享资源的时候,三个线程一起卖10张票,卖完就结束。
实现Runnable接口相对于Thread类的优势如下:
1)适合多个相同程序代码的线程去处理统一资源的情况
2)可以避免由于Java的单继承特性带来的局限
3)增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的

【7】实现Callable接口

上面两种创建线程的方法都有一个缺点:任务没有返回值。

【三种方法的特点】
1-使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递。但是不能多继承,只能继承Thread一个类,不够灵活。
2-使用Runnable方式,可以弥补无法多继承的缺点,但是只能使用主线程里面被声明为final的变量
3-前两种方法都不能拿到任务的返回结果,Callable接口可以。

【实例代码】

(三)线程的状态

要想实现多线程,必须在主线程中创建新的线程对象,所有线程都有五个状态,是创建、就绪、运行、阻塞、终止。

(1)创建状态:用构造方法创建一个线程对象后,新的线程对象便处于新建状态,已经有了响应的内存空间和除CPU资源以外的其他资源
(2)就绪状态:调用线程的start方法就可以启动线程,启动后线程进入就绪状态。此时,线程进入线程队列排序,等待CPU,已经具备运行条件
(3)运行状态:当就绪状态的线程被调用并且获得处理器资源时,线程就进入了运行状态,此时,自动调用该线程对象的run方法
(4)堵塞状态:一个正在执行的线程,爱被认为挂起或者需要执行耗时的输入/输出操作时,例如调用sleep、suspend、wait等方法时,线程会暂时中断自己的执行,并且让出CPU处理器,进入阻塞状态。当引起堵塞的原因被消除后,线程就可以再次进入排队队列,转入就绪状态。

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

(5)死亡状态:线程调用stop方法或者run方法执行结束后,进入死亡状态,不再具有继续运行的能力。


例题:
start方法只是表示线程进入了就绪状态,而run方法表示线程进入了运行状态。所以下面代码的运行结果可能是【111 thread 222 thread】、【111 222 thread thread】,程序运行的结果不稳定。所以说先执行start进入就绪的,不一定先执行run进入运行。

public class MyThread extends Thread{public static void main(String[] args){MyThread t=new MyThread ();MyThread s=new MyThread ();t.start();sout("111");s.start();sout("222");}public void run(){sout(thread);}
}

(四)线程操作的方法

【1】方法简介

  1. yield:线程礼让,暂停当前正在执行的线程对象。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 yield()只能使同优先级或更高优先级的线程有执行的机会。
  2. sleep(long var0):在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”。不推荐使用。sleep()使当前线程进入阻塞状态,在指定时间内不会执行。
  3. sleep(long var0, int var2)
  4. wait(): 在其他线程调用对象的notify或notifyAll方法前,导致当前线程等待。线程会释放掉它所占有的“锁标志”,从而使别的线程有机会抢占该锁。当前线程必须拥有当前对象锁。如果当前线程不是此锁的拥有者,会抛出IllegalMonitorStateException异常。
  5. Thread()
  6. Thread(Runnable var1)
  7. Thread(ThreadGroup var1, String var2)
  8. start():开始执行线程
  9. stop()
  10. interrupt():中断运行状态
  11. suspend()
  12. setPriority(int var1)
  13. getPriority()
  14. setName(String var1)
  15. getName():获取线程的名称
  16. join(long var1) throws InterruptedException:等待该线程终止。等待调用join方法的线程结束,再继续执行。如:t.join();//主要用于等待t线程运行结束,若无此句,main则会执行完毕,导致结果不可预测。
  17. synchronized void join(long var1):等待millis毫秒后,线程死亡
  18. toString()
  19. setPriority:设置线程的优先级,权限更高只能说明它获得CPU被执行的几率更大而已,并不是高优先级线程进入就绪状态就立刻占领CPU
  20. setDaemon:设置后台线程

【2】方法使用实例

(1)sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

线程调用sleep方法后会暂时让出指定时间的执行权,进入阻塞状态,但是不会让出所拥有的监视器资源也就是锁。当sleep时间结束后会从阻塞状态直接进入就绪状态,等待获取CPU资源。

在main方法和run方法里

Thread.sleep(1000);

(2)join():指等待t线程终止

join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。

Thread t = new Thread(); t.start(); t.join();

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了

(1)join案例一
【不加join】

package com.multithread.join;
class Thread1 extends Thread{private String name;public Thread1(String name) {super(name);this.name=name;}public void run() {System.out.println(Thread.currentThread().getName() + " 线程运行开始!");for (int i = 0; i < 5; i++) {System.out.println("子线程"+name + "运行 : " + i);try {sleep((int) Math.random() * 10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + " 线程运行结束!");}
}public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName()+"主线程运行开始!");Thread1 mTh1=new Thread1("A");Thread1 mTh2=new Thread1("B");mTh1.start();mTh2.start();System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");}}

输出结果:
main主线程运行开始!
main主线程运行结束!
B 线程运行开始!
子线程B运行 : 0
A 线程运行开始!
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程A运行 : 2
子线程A运行 : 3
子线程A运行 : 4
A 线程运行结束!
子线程B运行 : 2
子线程B运行 : 3
子线程B运行 : 4
B 线程运行结束!
发现主线程比子线程早结束

【加join】

public class Main {public static void main(String[] args) {System.out.println(Thread.currentThread().getName()+"主线程运行开始!");Thread1 mTh1=new Thread1("A");Thread1 mTh2=new Thread1("B");mTh1.start();mTh2.start();try {mTh1.join();} catch (InterruptedException e) {e.printStackTrace();}try {mTh2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");}}

运行结果:
main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
主线程一定会等子线程都结束了才结束

(2)join案例2

public class TestThreadJoin {public static void main(String[] args) {Hero gareen = new Hero("盖伦",616,100);Hero teemo = new Hero("提莫",300,130);Hero bh = new Hero("赏金猎人",350,165);Hero leesin = new Hero("盲僧",455,180);KillThread01 killThread01 = new KillThread01(gareen,teemo);KillThread01 killThread02 = new KillThread01(bh,leesin);killThread01.start();// 如果没有join的话,两个线程会轮流随机的获取// 但是加了join,会先把当前线程执行结束才会执行下一个线程// 主线程main会等待该线程结束完毕, 才会往下运行。try {killThread01.join();} catch (InterruptedException e) {e.printStackTrace();}killThread02.start();}
}

(3)yield():暂停当前正在执行的线程对象,并执行其他线程

Thread.yield()方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。可看上面的图。

package com.multithread.yield;
class ThreadYield extends Thread{public ThreadYield(String name) {super(name);}@Overridepublic void run() {for (int i = 1; i <= 50; i++) {System.out.println("" + this.getName() + "-----" + i);// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)if (i ==30) {this.yield();}}}
}public class Main {public static void main(String[] args) {ThreadYield yt1 = new ThreadYield("张三");ThreadYield yt2 = new ThreadYield("李四");yt1.start();yt2.start();}
}

运行结果:

第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。
第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。

【sleep()和yield()的区别】
sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。

sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程

另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

(4)setPriority(): 更改线程的优先级

MIN_PRIORITY = 1
NORM_PRIORITY = 5
MAX_PRIORITY = 10

用法:
Thread4 t1 = new Thread4(“t1”);
Thread4 t2 = new Thread4(“t2”);
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

(5)interrupt()

不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么这个线程还是不会中断的!

(6)wait()函数

(1)【wait函数是如何使用的】
首先,因为Object类是所有类的父类,java里把所有类都需要的方法函数达欧放到了Object类中,包括让线程通知和等待的一系列函数wait/notify等,都是放在Object类中的。

当一个线程调用一个共享变量的wait方法时,这个线程就会被阻塞挂起,从运行状态变为等待状态

(2)【在等待的线程什么时候才会返回】
1-其他线程调用了这个共享变量的notify/notifyAll函数
2-其他线程调用了自己的interrupt方法,让自己跑出InterruptdException异常

(3)【wait函数必须获取内置监视器锁后才能被调用】
当线程进入synchronized同步代码块/方法/类中,获取到监视器锁,才进入运行状态,只有在运行状态的线程才能调用wait函数进入等待状态,其他状态调用wait函数会跑出IllegalMonitor StateException

(4)【什么是虚假唤醒,又如何解决虚假唤醒的问题】
上面说了,当出现notify/notifyAll/interrupt/中断 这几种情况的时候,线程会从挂起等待状态变成运行状态,但是即使没有出现上面的情况,也会出现线程从挂起状态变成等待状态的情况,但是这时候即使变成运行状态了也不能获取共享变量的监视器锁,这就是虚假唤醒。

这种情况很少发生,但是也要防止,可以使用while循环判断条件,当条件不满足唤醒的时候,就重复的调用wait函数,保证线程在挂起的状态。

(5)【防止虚假唤醒问题的案例:生产者消费者问题】
例如生产者线程,会先使用synchronized拿到共享变量queue队列的监视器锁,然后判断当前队列是不是满的,如果队列是满的则说明现在没有空闲容量来存放生产者产生的任务的,那么当前的生产者就会调用queue对象的wait函数把线程挂起等待。为了防止出现虚假唤醒的情况,会用while循环不断判断当前queue对列是不是满的,如果还是满的,就重复调用wait方法让其挂起,这样即使出现虚假唤醒的问题也会被重新挂起。

synchronized(queue) {while(queue.size==MAX_SIZE){try{queue.wait();} catch (Exception ex) {。。。}queue.add(ele);queue.notifyAll();}
}

(6)【注意:当前线程调用共享变量的wait方法只会释放当前共享变量上的锁】
如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。

(7)【wait(long timeout)函数】
多了一个超时参数,如果一个线程调用共享对象的wait方法挂起以后,在指定的时间里没有被其他线程调用共享变量的notify、notifyAll方法唤醒,那么这个这个函数还是会因为超时返回。

如果把timeout设置为0,就等同于wait()函数,其实在wait()函数内部就是使用的wait(0)。

但是如果设置为负数,会报出参数异常。

(8)【notify函数】
一个线程调用共享对象的notify函数后,会唤醒一个在该共享变量上调用wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待线程是随机的。

被唤醒的线程,要等待唤醒它的线程释放共享变量上的监视器锁,然后和其他等待的线程一起竞争获取共享对象的监视器锁,只有当该线程竞争到了共享变量的监视器锁后才可以继续执行。

(9)【notifyAll函数】
notify函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll方法则会唤醒所有在该共享变量上由于调用wait系列方法而被挂起的线程。

并且notifyAll只会唤醒在调用这个方法前调用wait系列函数的线程,在这之后调用wait函数的线程都不会被唤醒。

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){…}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:

package com.multithread.wait;
public class MyThreadPrinter2 implements Runnable {   private String name;   private Object prev;   private Object self;   private MyThreadPrinter2(String name, Object prev, Object self) {   this.name = name;   this.prev = prev;   this.self = self;   }   @Override  public void run() {   int count = 10;   while (count > 0) {   synchronized (prev) {   synchronized (self) {   System.out.print(name);   count--;  self.notify();   }   try {   prev.wait();   } catch (InterruptedException e) {   e.printStackTrace();   }   }   }   }   public static void main(String[] args) throws Exception {   Object a = new Object();   Object b = new Object();   Object c = new Object();   MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);   MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);   MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);   new Thread(pa).start();Thread.sleep(100);  //确保按顺序A、B、C执行new Thread(pb).start();Thread.sleep(100);  new Thread(pc).start();   Thread.sleep(100);  }
}

输出结果:
ABCABCABCABCABCABCABCABCABCABC

先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假设依赖于JVM中线程调度、执行的顺序。

【3】线程操作方法总结

(1)wait和sleep区别

(1)共同点:
1-他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2-wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。

(2)不同点:
1-Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2-每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3-wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
   sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
  
wait()方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

(五)同步与死锁

如果一个多线程的程序是通过Runnable接口实现的,类中的属性就可以被多个线程共享了,共享就要考虑一个问题,如果多个线程要操作同一资源时就有可能出现资源的同步问题。例如多线程买票,可能出现卖多了的情况。

(5.1)使用同步解决

可以使用两种方法解决:同步代码块、同步方法(synchronized 修饰)
Synchronized的作用主要有三个:
1-确保线程互斥的访问同步代码
2-保证内存可见性
3-解决指令重排序问题

(1)同步代码块
用synchronized关键字修饰多线程类中的代码块
(2)同步方法
用synchronized关键字修饰多线程类中的整个方法

(5.3)认识关键字Synchronized和Volatile

(5.3.1)Volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。
  3. 由于有些时候对 volatile的操作,不会被保存,说明不会造成阻塞。
  4. 而对该变量的修改,volatile并不提供原子性的保证。由于及时更新,很可能导致另一线程访问最新变量值,无法跳出循环的情况

(5.3.2)Synchronized说明

synchronized很强大,既可以保证可见性,又可以保证原子性

(1)synchronized关键字的作用域有二种
1-是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2-是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。

(2)除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/区块/},它的作用域是当前对象;

(3)synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;

(4)补充说明
1-无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
2-每个对象只有一个锁(lock)与之相关联。
3-实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

(5.3.3)Synchronized使用实例

(1)把synchronized当作函数修饰符时
示例代码如下:

Public synchronized void methodAAA() {//….
}

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:
(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱

public void methodAAA() {synchronized (this)      //  (1){//…..}
}

【案例】把一些非线程安全容器里的方法改成安全方法
栈是非线程安全的,使用链表改成一个线程安全的栈

 LinkedList<T> values = new LinkedList<T>();//自定义方法,内嵌栈的方法,等于把栈的方法addLast改造成线程安全方法push//如过栈满了就通知线程等待,没满就唤醒线程继续存放public synchronized void push(T t) {values.addLast(t);}

(2)使用synchronized同步块

public void method3(SomeObject so) {synchronized(so){//…..}
}

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:

class Foo implements Runnable
{private byte[] lock = new byte[0];  // 特殊的instance变量Public void methodA(){synchronized(lock) { //… }}//…..
}

注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。


Object someObject =new Object();
synchronized (someObject){//此处的代码只有占有了someObject后才可以执行
}

(3)将synchronized作用于static 函数

Class Foo {public synchronized static void methodAAA()   // 同步的static 函数{//….}public void methodBBB(){synchronized(Foo.class)   //  class literal(类名称字面常量)}
}

代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。

可以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这个对象,而B的锁是Obj所属的那个Class。

(4)把非线程安全的结合转换成线程安全

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = Collections.synchronizedList(list1);

(5)总结一下
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的Class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

(5.3.4)两者的区别

  • volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
  • volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
  • volatile仅能实现变量的修改可见性,但不具备原子特性,而synchronized则可以保证变量的修改可见性和原子性.
  • volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
  • volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化.

(5.4)使用死锁解决

上面说的同步可以保证资源共享的正确性,但是过多的同步也会产生问题。那就是两个线程各自占有资源,并且抢夺对方的资源,最终陷入互相等待的情况。形成死锁。

【死锁案例】

public class TestThreadDeadlock {public static void main(String[] args) {final Hero ahri = new Hero("九尾妖狐");final Hero annie = new Hero("安妮");Thread thread1 = new Thread() {public void run(){synchronized (ahri) {System.out.println("thread1 已占有九尾妖狐");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("thread1 试图占有安妮");System.out.println("thread1 等待中 。。。。");//试图去占有synchronized (annie) {System.out.println("do something");}}}};thread1.start();Thread thread2 = new Thread() {public void run(){synchronized (annie) {System.out.println("thread1 已占有安妮");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("thread1 试图占有九尾妖狐");System.out.println("thread1 等待中 。。。。");//试图去占有synchronized (ahri) {System.out.println("do something");}}}};thread2.start();}
}

Java学习笔记---多线程并发相关推荐

  1. 0037 Java学习笔记-多线程-同步代码块、同步方法、同步锁

    什么是同步 在上一篇0036 Java学习笔记-多线程-创建线程的三种方式示例代码中,实现Runnable创建多条线程,输出中的结果中会有错误,比如一张票卖了两次,有的票没卖的情况,因为线程对象被多条 ...

  2. JAVA学习笔记(并发编程-叁)- 线程安全性

    文章目录 线程安全性-原子性 原子性-Atomic包 AtomicXXX: CAS, Unsafe.compareAndSwapInt AtomicLong LongAdder AtomicRefer ...

  3. Java学习笔记 --- 多线程

    一.线程相关概念 程序 程序是为完成特定任务,用某种语言编写的一组指令的集合.简单的说就是我们写的代码 进程 1.进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存 ...

  4. JAVA学习笔记 -- JUC并发编程

    1 进程.线程 进程就是用来加载指令.管理内存.管理IO.当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程.进程可以被视为程序的一个实例. 线程,一个线程就是一个指令流,将指令流 ...

  5. java学习笔记 多线程(一)创建多线程,线程常用方法

    首先是进程和线程的区别,进程就是像打开csgo.exe就是一个进程,然后打开LOL.exe又是另外一个进程了. 而线程呢,就是在同一进程内部,发生的事情. 那么就开始了解线程! 创建多线程: 线程有三 ...

  6. 0040 Java学习笔记-多线程-线程run()方法中的异常

    run()与异常 不管是Threade还是Runnable的run()方法都没有定义抛出异常,也就是说一条线程内部发生的checked异常,必须也只能在内部用try-catch处理掉,不能往外抛,因为 ...

  7. Java学习笔记---多线程同步的五种方法

    一.引言 前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊.闲话不多说,进入正题. 二.为什么要线程同步 因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会 ...

  8. java学习笔记 --- 多线程(多线程的控制)

    1.线程休眠    public static void sleep(long millis) public class ThreadSleep extends Thread {@Overridepu ...

  9. Java学习笔记5-1——多线程

    目录 前言 核心概念 线程创建 继承Thread类 实现Runnable接口 上述两个方法小结 实现Callable接口 并发问题简介 静态代理模式 线程状态 线程停止(stop) 线程休眠(slee ...

最新文章

  1. saxon java_如何将Saxon设置为Java中的Xslt处理器?
  2. Oracle中sys和system的区别
  3. yaahp层次分析法步骤_综合评价方法之层次分析法,选择再也难不倒你!
  4. Java 多线程练习---创建两个子线程,每个线程交替输出“你好--来自线程***”...
  5. opencv 图片叠加_基于OpenCV的红绿灯识别代码解析
  6. iptables的基础知识-iptables中的ICMP
  7. 诺,你们要的Python进阶来咯!【函数、类进阶必备】
  8. h5禁止页面长按操作_解决HTML5对手机页面长按会粘贴复制禁用的方法-H5教程
  9. TFT液晶屏、LCD显示屏40pin接口标准
  10. VR/AR时代最大的瓶颈是什么?
  11. 进安全模式提示”Press ENTER to continue loading SPTD.sys”
  12. Last packet sent to the server was 2 ms ago 解决办法
  13. vc项目开发:俄罗斯方块制作日志 [上]
  14. C#实现调用打印机(打印字符串、打印绘图、打印图片),还差打印水晶报表
  15. JS的map()方法会改变原始数组吗?
  16. 【每日一读】Large Scale Network Embedding: A Separable Approach
  17. 据说vite还是有坑,不行,那就还用vue-cli吧,命令vue create gua12,记一下,可能过一个星期不看,又忘了
  18. 迷宫游戏python实现
  19. 男人的魅力不在于财富,而在于精神深度
  20. 信息安全体系建设☞流量可视化(三)

热门文章

  1. 如何制作一个平台游戏
  2. 【虚拟仿真】Unity3D中实现控制物体的旋转、移动、缩放
  3. Python自动化办公:openpyxl教程(基础)
  4. 如何注册新加坡lol服务器,英雄联盟手游新加坡服安卓账号怎么注册
  5. PyCharm安装后启动时APPCRASH问题的解决
  6. Windows Terminal在设置里把窗口模式改成专注以后要怎么调回来
  7. 博计报表数据源找不到的一个原因
  8. 利用R语言OLS回归分析
  9. 【MAPBOX基础功能】16、mapbox叠加图片图层到地图上
  10. 云网端融合形成新计算体系,催生云上新物种