一、线程的安全问题
1、问题的发现
当有多个线程同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全的。
下面通过一个案例来演示线程的安全问题。
模拟电影票买票的过程,其中,一共有100张票。下面来模拟电影票的售票窗口,实现多个窗口同时卖票,采用线程对象来模拟,通过实现 Runnable 接口子类来模拟。
示例:
1 public class WindowTest1 {
2 public static void main(String[] args) {
3 Window w = new Window();
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 implements Runnable {
17
18 private int ticket = 100;
19
20 @Override
21 public void run() {
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 class WindowTest1 {
2 public static void main(String[] args) {
3 Window w = new Window();
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 implements Runnable {
17
18 private int ticket = 100;
19 //创建一个 同步监视器,锁对象
20 private Object obj = new Object();
21
22 @Override
23 public void run() {
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 class WindowTest1 {
2 public static void main(String[] args) {
3 Window w = new Window();
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 implements Runnable {
17
18 private int ticket = 100;
19
20 @Override
21 public void run() {
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 class WindowTest2 {
2 public static void main(String[] args) {
3 Window2 t1 = new Window2();
4 Window2 t2 = new Window2();
5 Window2 t3 = new Window2();
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 extends Thread {
18 private static int ticket = 100;
19 private static Object obj = new Object();
20
21 @Override
22 public void run() {
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 class WindowTest2 {
2 public static void main(String[] args) {
3 Window2 t1 = new Window2();
4 Window2 t2 = new Window2();
5 Window2 t3 = new Window2();
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 extends Thread {
18 private static int ticket = 100;
19
20 @Override
21 public void run() {
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 class WindowTest3 {
2 public static void main(String[] args) {
3 Window3 w = new Window3();
4
5 Thread t1 = new Thread(w);
6 Thread t2 = new Thread(w);
7 Thread t3 = new Thread(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 implements Runnable {
20 private int ticket = 100;
21
22
23 @Override
24 public void run() {
25 while (true) {
26 show();
27 }
28 }
29
30 private synchronized void show() { //同步监视器:this
31 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 class WindowTest4 {
2 public static void main(String[] args) {
3 Window4 t1 = new Window4();
4 Window4 t2 = new Window4();
5 Window4 t3 = new Window4();
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 extends Thread {
20 private static int ticket = 100;
21
22 @Override
23 public void run() {
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 class WindowTest5 {
2 public static void main(String[] args) {
3 Window5 w = new Window5();
4
5 Thread t1 = new Thread(w);
6 Thread t2 = new Thread(w);
7 Thread t3 = new Thread(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 implements Runnable {
20
21 private int ticket = 100;
22 //1.实例化ReentrantLock
23 private ReentrantLock lock = new ReentrantLock();
24
25 @Override
26 public void run() {
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 class WindowTest6 {
2 public static void main(String[] args) {
3 Window6 w1 = new Window6();
4 Window6 w2 = new Window6();
5 Window6 w3 = new Window6();
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 extends Thread {
18
19 private static int ticket = 100;
20 //1.实例化ReentrantLock
21 private static ReentrantLock lock = new ReentrantLock();
22
23 @Override
24 public void run() {
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、死锁的必要条件
死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件
3、解决方法
(1)专门的算法、原则;
(2)尽量减少同步资源的定义;
(3)尽量避免嵌套同步
4、示例
示例一:
1 //死锁的演示
2 class A {
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 class B {
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 implements Runnable {
41 A a = new A();
42 B b = new B();
43
44 public void init() {
45 Thread.currentThread().setName("主线程");
46 // 调用a对象的foo方法
47 a.foo(b);
48 System.out.println("进入了主线程之后");
49 }
50
51 public void run() {
52 Thread.currentThread().setName("副线程");
53 // 调用b对象的bar方法
54 b.bar(a);
55 System.out.println("进入了副线程之后");
56 }
57
58 public static void main(String[] args) {
59 DeadLock1 dl = new DeadLock1();
60 new Thread(dl).start();
61
62
63 dl.init();
64 }
65 }
示例二:
1 public class DeadLock2 {
2 public static void main(String[] args) {
3
4 StringBuffer s1 = new StringBuffer();
5 StringBuffer s2 = new StringBuffer();
6
7
8 new Thread() {
9 @Override
10 public void run() {
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(new Runnable() {
37 @Override
38 public void run() {
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 }