1.什么是线程安全问题?
从某个线程开始访问到访问结束的整个过程,如果有一个访问对象被其他线程修改,那么对于当前线程而言就发生了线程安全问题;
如果在整个访问过程中,无一对象被其他线程修改,就是线程安全的,即存在两个或者两个以上的线程对象共享同一个资源
2.线程安全问题产生的根本原因
首先是多线程环境,即同时存在有多个操作者,单线程环境不存在线程安全问题。在单线程环境下,任何操作包括修改操作都是操作者自己发出的,
操作者发出操作时不仅有明确的目的,而且意识到操作的影响。
多个操作者(线程)必须操作同一个对象,只有多个操作者同时操作一个对象,行为的影响才能立即传递到其他操作者。
多个操作者(线程)对同一对象的操作必须包含修改操作,共同读取不存在线程安全问题,因为对象不被修改,未发生变化,不能产生影响。
综上可知,线程安全问题产生的根本原因是共享数据存在被并发修改的可能,即一个线程读取时,允许另一个线程修改
3.有线程安全的实例
模拟火车站售票窗口,开启三个窗口售票,总票数为20张
实例一:
package com.practise.threadsafe; //模拟火车站售票窗口,开启三个窗口售票,总票数为100张 //存在线程的安全问题 class Window extends Thread { static int ticket = 20; public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } else { break; } } } } public class TestWindow { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口1"); w2.setName("窗口2"); w3.setName("窗口3"); w1.start(); w2.start(); w3.start(); } }
运行结果的一种:出现重复售票及负数票
窗口3售票,票号为:20
窗口2售票,票号为:18
窗口1售票,票号为:19
窗口1售票,票号为:17
窗口3售票,票号为:16
窗口2售票,票号为:17
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口1售票,票号为:10
窗口3售票,票号为:8
窗口2售票,票号为:9
窗口1售票,票号为:7
窗口1售票,票号为:6
窗口2售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口3售票,票号为:4
窗口2售票,票号为:3
窗口2售票,票号为:2
窗口1售票,票号为:2
窗口3售票,票号为:2
窗口2售票,票号为:1
窗口1售票,票号为:-1
窗口3售票,票号为:0
实例二:
package com.practise.threadsafe; //使用实现Runnable接口的方式,售票 /* * 此程序存在线程的安全问题 */ class Window1 implements Runnable { int ticket = 20; public void run() { while (true) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } else { break; } } } } public class TestWindow1 { public static void main(String[] args) { Window1 w = new Window1(); 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(); } }
运行结果的一种:出现重复售票
窗口2售票,票号为:20
窗口1售票,票号为:20
窗口3售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口3售票,票号为:14
窗口1售票,票号为:15
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口2售票,票号为:10
窗口1售票,票号为:10
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口3售票,票号为:7
窗口2售票,票号为:8
窗口2售票,票号为:6
窗口3售票,票号为:4
窗口1售票,票号为:5
窗口3售票,票号为:3
窗口1售票,票号为:3
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:0
窗口2售票,票号为:1
4.线程安全解决机制Lock和synchronized
4.1 同步代码块synchronized
package com.practise.threadsafe; /* 同步代码块 * synchronized(同步监视器){ * //需要被同步的代码块(即为操作共享数据的代码) * } * 1.共享数据:多个线程共同操作的同一个数据(变量) * 2.同步监视器:由一个类的对象来充当。哪个线程获取此监视器,谁就执行大括号里被同步的代码。俗称:锁 * 要求:所有的线程必须共用同一把锁! * 注:在实现的方式中,考虑同步的话,可以使用this来充当锁。但是在继承的方式中,慎用this! */ class Window implements Runnable { int ticket = 20;// 共享数据 public void run() { while (true) { // this表示当前对象,本题中即为w synchronized (this) { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } } } public class TestWindow { public static void main(String[] args) { Window w = new Window(); 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(); } }
运行结果的一种:
窗口1售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口2售票,票号为:17
窗口2售票,票号为:16
窗口2售票,票号为:15
窗口2售票,票号为:14
窗口2售票,票号为:13
窗口2售票,票号为:12
窗口3售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口1售票,票号为:8
窗口3售票,票号为:7
窗口2售票,票号为:6
窗口2售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口3售票,票号为:1
4.2 同步方法synchronized
package com.practise.threadsafe; /* * 同步方法 * 将操作共享数据的方法声明为synchronized。即此方法为同步方法,能够保证当其中一个线程执行 * 此方法时,其它线程在外等待直至此线程执行完此方法 */ class Window1 implements Runnable { int ticket = 20;// 共享数据 public void run() { while (true) { show(); } } public synchronized void show() { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } } public class TestWindow1 { public static void main(String[] args) { Window1 w = new Window1(); 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(); } }
运行结果的一种:
窗口2售票,票号为:20
窗口1售票,票号为:19
窗口3售票,票号为:18
窗口1售票,票号为:17
窗口2售票,票号为:16
窗口1售票,票号为:15
窗口3售票,票号为:14
窗口1售票,票号为:13
窗口2售票,票号为:12
窗口1售票,票号为:11
窗口3售票,票号为:10
窗口1售票,票号为:9
窗口2售票,票号为:8
窗口1售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口1售票,票号为:4
窗口2售票,票号为:3
窗口1售票,票号为:2
窗口3售票,票号为:1
4.3 同步锁Lock
package com.practise.threadsafe; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Window2 implements Runnable { int ticket = 20;// 共享数据 Lock lock = new ReentrantLock(); public void run() { while (true) { lock.lock(); // 获取锁 try { if (ticket > 0) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售票,票号为:" + ticket--); } } finally { lock.unlock(); // 释放锁 } } } } public class TestWindow2 { public static void main(String[] args) { Window2 w = new Window2(); 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(); } }
运行结果的一种:
窗口3售票,票号为:20
窗口3售票,票号为:19
窗口3售票,票号为:18
窗口3售票,票号为:17
窗口3售票,票号为:16
窗口3售票,票号为:15
窗口3售票,票号为:14
窗口3售票,票号为:13
窗口3售票,票号为:12
窗口1售票,票号为:11
窗口2售票,票号为:10
窗口3售票,票号为:9
窗口3售票,票号为:8
窗口3售票,票号为:7
窗口3售票,票号为:6
窗口3售票,票号为:5
窗口3售票,票号为:4
窗口3售票,票号为:3
窗口3售票,票号为:2
窗口1售票,票号为:1
5.synchronized 的局限性 与 Lock 的优点
如果一个代码块被synchronized关键字修饰,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待直至占有锁的线程释放锁。事实上,占有锁的线程释放锁一般会是以下三种情况之一:
- 占有锁的线程执行完了该代码块,然后释放对锁的占有;
- 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
- 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
synchronized 是Java语言的内置特性,可以轻松实现对临界资源的同步互斥访问。那么,为什么还会出现Lock呢?试考虑以下三种情况:
Case 1 :
在使用synchronized关键字的情形下,假如占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法。这会极大影响程序执行效率。因此,就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间 (解决方案:tryLock(long time, TimeUnit unit)) 或者 能够响应中断 (解决方案:lockInterruptibly())),这种情况可以通过 Lock 解决。
Case 2 :
我们知道,当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象。但是如果采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作。因此,需要一种机制来使得当多个线程都只是进行读操作时,线程之间不会发生冲突。同样地,Lock也可以解决这种情况 (解决方案:ReentrantReadWriteLock) 。
Case 3 :
我们可以通过Lock得知线程有没有成功获取到锁 (解决方案:ReentrantLock) ,但这个是synchronized无法办到的。
上面提到的三种情形,我们都可以通过Lock来解决,但 synchronized 关键字却无能为力。事实上,Lock 是 java.util.concurrent.locks包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。也就是说,Lock提供了比synchronized更多的功能。但是要注意以下几点:
- 1)synchronized是Java的关键字,因此是Java的内置特性,是基于JVM层面实现的。而Lock是一个Java接口,是基于JDK层面实现的,通过这个接口可以实现同步访问;
- 2)采用synchronized方式不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致死锁现象
6.Lock和synchronized的选择
总结来说,Lock和synchronized有以下几点不同:
- 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- 2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
- 3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
- 5)Lock可以提高多个线程进行读操作的效率
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择