什么是线程同步
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。这里的同步千万不要理解成那个同时进行,应是指协同、协助、互相配合。线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!
为什么要实现线程同步
模拟火车站售票程序,开启三个窗口售票。
class Window implements Runnable{
//假设一共有100张票
private static int tictet = 100;
@Override
public void run() {
while (true){
if (tictet>0){
try {
// 提高JVM切换线程的概率
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//输出当前线程名和票号
System.out.println(Thread.currentThread().getName() + "票号为:" + tictet--);
}
else {
break;
}
}
}
}
@Test
public void ThreadSynchronizationTest() throws InterruptedException {
Window window = new Window();
Thread thread1 = new Thread(window);
Thread thread2 = new Thread(window);
Thread thread3 = new Thread(window);
thread1.setName("窗口一");
thread2.setName("窗口二");
thread3.setName("窗口三");
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(1000);
}
}
输出
窗口一票号为:99
窗口三票号为:99
窗口二票号为:100
窗口三票号为:98
窗口一票号为:98
窗口二票号为:98
窗口一票号为:97
窗口二票号为:96
窗口三票号为:95
...
从输出中可以看出,代码在运行过程中同时访问了共享的ticket变量,导致出现了重复的票号。
总结来说,当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
如何实现线程同步
同步锁机制:
在《Thinking in Java》 中, 是这么说的:对于并发工作, 你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争) 。 防止这种冲突的方法就是当资源被一个任务使用时, 在其上加锁。 第一个访问某项资源的任务必须锁定这项资源, 使其他任务在其被解锁之前, 就无法访问它了, 而在其被解锁之时, 另一个任务就可以锁定并使用它了。
synchronized的锁是什么?
- 任意对象都可以作为同步锁。 所有对象都自动含有单一的锁(监视器) 。
- 同步方法的锁:静态方法(类名.class) 、 非静态方法(this)
- 同步代码块:自己指定, 很多时候也是指定为this或类名.class
注意:
- 必须确保使用同一个资源的多个线程共用一把锁, 这个非常重要, 否则就
- 无法保证共享资源的安全
- 一个线程类中的所有静态方法共用同一把锁(类名.class) , 所有非静态方法共用同一把锁(this) , 同步代码块(指定需谨慎)
释放锁的操作
- 当前线程的同步方法、同步代码块执行结束。
- 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的继续执行。
- 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception, 导致异常结束。
- 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
- 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
- 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
- 应尽量避免使用suspend()和resume()来控制线程
实现线程同步
实现一:使用synchronized
- 同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
- synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){
….
}
使用线程同步改进售票问题
class Window implements Runnable{
private static int tictet = 100;
@Override
public void run() {
while (true){
//这里的this是同一个Window对象
synchronized (this) {
if (tictet > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为:" + tictet--);
} else {
break;
}
}
}
}
}
实现二:使用Lock
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的
工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。
使用线程同步改进售票问题
class Window implements Runnable{
private static int tictet = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
//加锁
lock.lock();
if (tictet > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为:" + tictet--);
} else {
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
}
}
}
注意:如果同步代码有异常,要将unlock()写入finally语句块
synchronized 与 Lock 的对比
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁, synchronized有代码块锁和方法锁
- 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)
线程的通信
如果有这样一个问题
使用两个线程打印 1-100。线程1, 线程2 交替打印
那么之前的只是显然无法满足需要,这时候就需要用到线程的通信技术
线程通信中常用的函数
- wait() 与 notify() 和 notifyAll()
- wait():令当前线程挂起并放弃CPU、 同步资源并等待, 使别的线程可访问并修改共享源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
- wait():令当前线程挂起并放弃CPU、 同步资源并等待, 使别的线程可访问并修改共享源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。
- 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
- 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。
wait() 方法
- 在当前线程中调用方法: 对象名.wait()
- 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify(或notifyAll) 为止。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
- 调用此方法后,当前线程将释放对象监控权 ,然后进入等待
- 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。
notify()/notifyAll()
- 在当前线程中调用方法: 对象名.notify()
- 功能:唤醒等待该对象监控权的一个/所有线程。
- 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
例题实现
class Numbers implements Runnable{
private int num=1;
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj) {
obj.notify();
if (num <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + num++);
}
else {
break;
}
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
@Test
public void CommunicationTest1() throws InterruptedException {
Numbers numbers = new Numbers();
Thread thread1 = new Thread(numbers);
Thread thread2 = new Thread(numbers);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
输出
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
...