一、线程的安全问题
1、问题的发现
当有多个线程同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全的。
下面通过一个案例来演示线程的安全问题。
模拟电影票买票的过程,其中,一共有100张票。下面来模拟电影票的售票窗口,实现多个窗口同时卖票,采用线程对象来模拟,通过实现 Runnable 接口子类来模拟。
示例:
1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4
5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8
9 //同时卖票
10 t1.start();11 t2.start();12 t3.start();13 }14 }15
16 class Window implementsRunnable {17
18 private int ticket = 100;19
20 @Override21 public voidrun() {22
23 while (true) {24 //有票,可以出售
25 if (ticket > 0) {26
27 //出票操作,使用 sleep 模拟一下出票时间
28 try{29 Thread.sleep(100);30 } catch(InterruptedException e) {31 e.printStackTrace();32 }33 //获取当前线程对象的名字
34 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);35
36
37 ticket--;38 } else{39 break;40 }41 }42 }43 }
运行结果,发现会有这样的现象发生:
(1)错票
(2)重票
在运行结果中可以看到会有两个问题发生:
① 相同的票数,比如100这张票被卖了两次;
② 不存在的票,比如 0 和 -1 票,是不存在的;
2、分析问题
针对于上面的售票现象,为什么会出现这样的情况呢?
当只有一个窗口售票或多个窗口分别出售自己的票是没有问题的。但是当三个窗口,同时访问共享的资源,就会导致线程不同步,这种问题称为 线程不安全。
线程安全问题的产生的原理:
可以发现多个线程执行的不确定性引起执行结果的不稳定;
多个线程对公共的数据处理,会造成操作的不完整性,会破坏数据。
注意:线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
3、问题的总结
问题出现的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
如何解决:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
二、同步机制
Java 对于多线程的安全问题提供了专业的解决方式:同步机制
针对上面的售票案例简单描述一下同步机制:
当窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,窗口1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。
也就是说某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
同步方式的分析:
1、同步的方式,解决了线程的安全问题(好处)
2、操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。(局限性)
同步的注意项:
(1)操作共享数据的代码,即为需要被同步的代码加锁;
不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)
(2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。
(3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
三、方式一:同步代码块
1、语法格式
synchronized (对象){
// 需要被同步的代码 / 可能会出现线程安全问题的代码(访问共享数据的代码)
}
2、实现方式使用同步代码块
代码示例:
1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4
5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8
9 //同时卖票
10 t1.start();11 t2.start();12 t3.start();13 }14 }15
16 class Window implementsRunnable {17
18 private int ticket = 100;19 //创建一个 同步监视器,锁对象
20 private Object obj = newObject();21
22 @Override23 public voidrun() {24
25 while (true) {26 synchronized(obj) { //使用 synchronized 给操作共享数据的地方加锁27 //有票,可以出售
28 if (ticket > 0) {29
30 //出票操作,使用 sleep 模拟一下出票时间
31 try{32 Thread.sleep(100);33 } catch(InterruptedException e) {34 e.printStackTrace();35 }36 //获取当前线程对象的名字
37 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);38
39
40 ticket--;41 } else{42 break;43 }44 }45 }46 }47 }
如果加锁都需要创建一个对象,我们可以改进一下:
1 public classWindowTest1 {2 public static voidmain(String[] args) {3 Window w = newWindow();4
5 Thread t1 = new Thread(w, "窗口1");6 Thread t2 = new Thread(w, "窗口2");7 Thread t3 = new Thread(w, "窗口3");8
9 //同时卖票
10 t1.start();11 t2.start();12 t3.start();13 }14 }15
16 class Window implementsRunnable {17
18 private int ticket = 100;19
20 @Override21 public voidrun() {22
23 while (true) {24 synchronized (this) { //方式二:此时的this:唯一的Window1的对象25 //有票,可以出售
26 if (ticket > 0) {27
28 //出票操作,使用 sleep 模拟一下出票时间
29 try{30 Thread.sleep(100);31 } catch(InterruptedException e) {32 e.printStackTrace();33 }34 //获取当前线程对象的名字
35 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);36
37
38 ticket--;39 } else{40 break;41 }42 }43 }44 }45 }
注意:这里的 this指的就是当前的 Window 对象,因为在main方法中多个 Thread 共用了同一个 Window 对象,所以这里的this 是公共的锁。
3、继承方式使用同步代码块
1 public classWindowTest2 {2 public static voidmain(String[] args) {3 Window2 t1 = newWindow2();4 Window2 t2 = newWindow2();5 Window2 t3 = newWindow2();6
7 t1.setName("窗口1");8 t2.setName("窗口2");9 t3.setName("窗口3");10
11 t1.start();12 t2.start();13 t3.start();14 }15 }16
17 class Window2 extendsThread {18 private static int ticket = 100;19 private static Object obj = newObject();20
21 @Override22 public voidrun() {23
24 while (true) {25 synchronized(obj) {26 if (ticket > 0) {27
28 try{29 Thread.sleep(100);30 } catch(InterruptedException e) {31 e.printStackTrace();32 }33
34 System.out.println(getName() + ":卖票,票号为:" +ticket);35 ticket--;36 } else{37 break;38 }39 }40 }41 }42 }
对于继承的方式,如果我们也想避免创建的繁琐,可以这样写:
1 public classWindowTest2 {2 public static voidmain(String[] args) {3 Window2 t1 = newWindow2();4 Window2 t2 = newWindow2();5 Window2 t3 = newWindow2();6
7 t1.setName("窗口1");8 t2.setName("窗口2");9 t3.setName("窗口3");10
11 t1.start();12 t2.start();13 t3.start();14 }15 }16
17 class Window2 extendsThread {18 private static int ticket = 100;19
20 @Override21 public voidrun() {22
23 while (true) {24 synchronized (Window2.class) { //方式2
25 if (ticket > 0) {26
27 try{28 Thread.sleep(100);29 } catch(InterruptedException e) {30 e.printStackTrace();31 }32
33 System.out.println(getName() + ":卖票,票号为:" +ticket);34 ticket--;35 } else{36 break;37 }38 }39 }40 }41 }
注意:这里是不能使用 this 的,因为在 main 中创建了多个 Window2对象,它们各不一样。但是我们可以使用当前类的对象,全局唯一的类对象来充当锁。因为类对象只会加载一次,是全局唯一的。
4、小结
(1)操作共享数据的代码,即为需要被同步的代码加锁;
不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)
(2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。
(3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
要求:多个线程必须要共用同一把锁。
注意:(具体情况还要具体分析!)
① 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
② 在继承Thread类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。
四、方式二:同步方法
1、语法格式
public static synchronized void show (String name){
可能会产生线程安全问题的代码 / 可能会出现线程安全问题的代码(访问了共享数据的代码)
}
synchronized还可以放在方法声明中,表示整个方法为同步方法。
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
2、实现方式使用同步方法
代码示例:
1 public classWindowTest3 {2 public static voidmain(String[] args) {3 Window3 w = newWindow3();4
5 Thread t1 = newThread(w);6 Thread t2 = newThread(w);7 Thread t3 = newThread(w);8
9 t1.setName("窗口1");10 t2.setName("窗口2");11 t3.setName("窗口3");12
13 t1.start();14 t2.start();15 t3.start();16 }17 }18
19 class Window3 implementsRunnable {20 private int ticket = 100;21
22
23 @Override24 public voidrun() {25 while (true) {26 show();27 }28 }29
30 private synchronized voidshow() { //同步监视器:this31 if (ticket > 0) {32
33 try{34 Thread.sleep(100);35 } catch(InterruptedException e) {36 e.printStackTrace();37 }38
39 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);40
41 ticket--;42 }43 }44 }
注意:这里的锁对象是 this,因为在 main 方法中还是共用了同一个 Window3 对象,这里的 this 就是此对象,可以公共的锁对象。如果在 main 中创建了多个 Window3 对象并传递给 Thread来启动,这样并不可以保证同步哦!
3、继承方式使用同步方法
代码示例:
1 public classWindowTest4 {2 public static voidmain(String[] args) {3 Window4 t1 = newWindow4();4 Window4 t2 = newWindow4();5 Window4 t3 = newWindow4();6
7
8 t1.setName("窗口1");9 t2.setName("窗口2");10 t3.setName("窗口3");11
12 t1.start();13 t2.start();14 t3.start();15
16 }17 }18
19 class Window4 extendsThread {20 private static int ticket = 100;21
22 @Override23 public voidrun() {24 while (true) {25 show();26 }27 }28
29 private static synchronized void show() { //同步监视器:Window4.class
30 if (ticket > 0) {31
32 try{33 Thread.sleep(100);34 } catch(InterruptedException e) {35 e.printStackTrace();36 }37
38 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" +ticket);39 ticket--;40 }41 }42 }
注意:在继承中这样使用的锁对象就是 Window4.class,当前的类对象。
切记不能写成这样
//private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
这样他们的锁对象就不再是类对象,而是以每个实例对象为锁的,并不是公共的锁,不能保证同步。
4、小结
(1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
(2)非静态的同步方法,同步监视器是实现类对象:this;
静态的同步方法,同步监视器是当前类对象:当前类本身
五、同步的总结
1、分析同步原理
2、同步机制中的锁
(1)同步锁机制
(2)synchronized 的锁是什么?
① 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。
② 同步方法的锁:静态方法(类名.class)、非静态方法(this)
③ 同步代码块:自己指定,很多时候也是指定为 this 或 类名.class
(3)注意
① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则无法保证共享资源的安全;
② 一个线程类中的所有静态方法共用一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)
3、同步的范围
(1)如何找问题,即代码是否存在线程安全?(重要)
① 明确哪些代码是多线程运行的代码;
② 明确多个线程是否有共享数据;
③ 明确多线程运行代码中是否有多条语句操作共享数据;
(2)如果解决?(重要)
① 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行;
② 即所有操作共享数据的这些语句都要放在同步范围中;
(3)切记
① 范围太小:没锁住所有有安全问题的代码;
② 范围太大:没发挥多线程的功能;
4、释放锁的操作
(1)当前线程的同步方法、同步代码块执行结束;
(2)当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行;
(3)当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束;
(4)当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。
5、不会释放锁的操作
(1)线程执行同步代码块或同步方式时,程序调用 Thread.sleep()、Thread.yield()方法暂停当前线程的执行;
(2)线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(不同监视器);
应尽量避免使用 suspend() 和 resume() 来控制线程;
六、Lock 锁——JDK5.0 新增锁
1、Lock 锁
(1)从 JDK5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步,同步锁使用 Lock 对象充当;
(2)java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
(3)ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。
2、语法
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
lock.lock();
try{
//保证线程安全的代码;
}
finally{
lock.unlock();
}
}
}
注意:如果同步代码有异常,要将unlock( )写入finally语句块
3、实现方式使用 Lock 锁
使用步骤:
① 在成员位置创建一个ReentrantLock对象(Lock接口的一个实现类)
② 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
③ 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
示例:
1 public classWindowTest5 {2 public static voidmain(String[] args) {3 Window5 w = newWindow5();4
5 Thread t1 = newThread(w);6 Thread t2 = newThread(w);7 Thread t3 = newThread(w);8
9 t1.setName("窗口1");10 t2.setName("窗口2");11 t3.setName("窗口3");12
13 t1.start();14 t2.start();15 t3.start();16 }17 }18
19 class Window5 implementsRunnable {20
21 private int ticket = 100;22 //1.实例化ReentrantLock
23 private ReentrantLock lock = newReentrantLock();24
25 @Override26 public voidrun() {27 while (true) {28 try{29 //2.调用锁定方法 lock()
30 lock.lock();31
32 if(ticket > 0){33
34 try{35 Thread.sleep(100);36 } catch(InterruptedException e) {37 e.printStackTrace();38 }39
40 System.out.println(Thread.currentThread().getName() + ":售票,票号为:" +ticket);41 ticket--;42 }else{43 break;44 }45 } finally{46 //3.调用解锁方法:unlock(),无论程序是否异常,都会把锁释放
47 lock.unlock();48 }49 }50 }51 }
4、继承方式使用 Lock 锁
代码示例:
1 public classWindowTest6 {2 public static voidmain(String[] args) {3 Window6 w1 = newWindow6();4 Window6 w2 = newWindow6();5 Window6 w3 = newWindow6();6
7 w1.setName("窗口一");8 w2.setName("窗口二");9 w3.setName("窗口三");10
11 w1.start();12 w2.start();13 w3.start();14 }15 }16
17 class Window6 extendsThread {18
19 private static int ticket = 100;20 //1.实例化ReentrantLock
21 private static ReentrantLock lock = newReentrantLock();22
23 @Override24 public voidrun() {25 while (true) {26 try{27 //2.调用锁定方法 lock()
28 lock.lock();29
30 if(ticket > 0){31
32 try{33 Thread.sleep(100);34 } catch(InterruptedException e) {35 e.printStackTrace();36 }37
38 System.out.println(Thread.currentThread().getName() + ":售票,票号为:" +ticket);39 ticket--;40 }else{41 break;42 }43 } finally{44 //3.调用解锁方法:unlock()
45 lock.unlock();46 }47 }48 }49 }
5、synchronized 与 Lock 的对比
(1)Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用自动释放;
(2)Lock 只有代码块锁,synchronized 有代码块锁和方法所;
(3)使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock ——>同步代码块(已经进入了方法体,分配了相应资源)——>同步方法(在方法体之外)
七、线程的死锁问题
1、死锁
(1)不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;
(2)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;
(3)死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
2、死锁的必要条件
死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件
1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
3、解决方法
(1)专门的算法、原则;
(2)尽量减少同步资源的定义;
(3)尽量避免嵌套同步
4、示例
示例一:
1 //死锁的演示
2 classA {3 public synchronized void foo(B b) { //同步监视器:A类的对象:a
4 System.out.println("当前线程名: " +Thread.currentThread().getName()5 + " 进入了A实例的foo方法"); //①
6 try{7 Thread.sleep(200);8 } catch(InterruptedException ex) {9 ex.printStackTrace();10 }11 System.out.println("当前线程名: " +Thread.currentThread().getName()12 + " 企图调用B实例的last方法"); //③
13 b.last();14 }15
16 public synchronized void last() {//同步监视器:A类的对象:a
17 System.out.println("进入了A类的last方法内部");18 }19 }20
21 classB {22 public synchronized void bar(A a) {//同步监视器:b
23 System.out.println("当前线程名: " +Thread.currentThread().getName()24 + " 进入了B实例的bar方法"); //②
25 try{26 Thread.sleep(200);27 } catch(InterruptedException ex) {28 ex.printStackTrace();29 }30 System.out.println("当前线程名: " +Thread.currentThread().getName()31 + " 企图调用A实例的last方法"); //④
32 a.last();33 }34
35 public synchronized void last() {//同步监视器:b
36 System.out.println("进入了B类的last方法内部");37 }38 }39
40 public class DeadLock1 implementsRunnable {41 A a = newA();42 B b = newB();43
44 public voidinit() {45 Thread.currentThread().setName("主线程");46 //调用a对象的foo方法
47 a.foo(b);48 System.out.println("进入了主线程之后");49 }50
51 public voidrun() {52 Thread.currentThread().setName("副线程");53 //调用b对象的bar方法
54 b.bar(a);55 System.out.println("进入了副线程之后");56 }57
58 public static voidmain(String[] args) {59 DeadLock1 dl = newDeadLock1();60 newThread(dl).start();61
62
63 dl.init();64 }65 }
示例二:
1 public classDeadLock2 {2 public static voidmain(String[] args) {3
4 StringBuffer s1 = newStringBuffer();5 StringBuffer s2 = newStringBuffer();6
7
8 newThread() {9 @Override10 public voidrun() {11
12 synchronized(s1) {13
14 s1.append("a");15 s2.append("1");16
17 try{18 Thread.sleep(100);19 } catch(InterruptedException e) {20 e.printStackTrace();21 }22
23
24 synchronized(s2) {25 s1.append("b");26 s2.append("2");27
28 System.out.println(s1);29 System.out.println(s2);30 }31 }32 }33 }.start();34
35
36 new Thread(newRunnable() {37 @Override38 public voidrun() {39 synchronized(s2) {40
41 s1.append("c");42 s2.append("3");43
44 try{45 Thread.sleep(100);46 } catch(InterruptedException e) {47 e.printStackTrace();48 }49
50 synchronized(s1) {51 s1.append("d");52 s2.append("4");53
54 System.out.println(s1);55 System.out.println(s2);56 }57 }58 }59 }).start();60 }61
62 }