前言
上一期分享了一些关于多线程的基础的知识,然后有提到线程安全问题,是由于没有提供数据访问保护,导致多个线程先后修改共享数据,而没有同步,导致读到的数据是“脏”数据。既然是因为数据没有同步造成的,那么我们就聊一聊如何实现线程同步。
一、什么是线程同步?
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
二、线程同步的4种方法
1.synchronized 关键字
先来介绍一下这个synchronized关键字。它是一个java中的关键字,它可以用来修饰方法/代码块,它的作用就是可以保证同一时刻最多只有一个线程执行被synchronized修饰的方法/代码块。属于悲观锁。它实现同步的方式也很简单,只需要将可能会发生线程不安全的代码用synchronized关键字包起来就好。
还是售票系统这个例子,代码如下:
public class Thread06 {
public static void main(String[] args) {
Count1 count = new Count1();
Thread t1 = new Thread(count, "线程1======");
Thread t2 = new Thread(count, "线程2");
t1.start();
t2.start();
}
}
class Count1 implements Runnable {
private int count = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (count == 0) {
break;
}
try {
//增加出现线程不安全情况的几率,为了演示方便
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + "(" + count + ")");
}
}
}
}
通过synchronized来同步count–这个操作,保证当前线程操作完了,别的线程才能进行操作。运行结果如下:
2.wait()、notify()和notifyAll()方法
方法名 | 方法描述 |
---|---|
wait() | 让当前线程进入等待状态。直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。 |
notify() | 唤醒在该对象上等待的某个线程 |
notifyAll() | 唤醒在该对象上等待的所有线程 |
synchronized锁是悲观锁,被它修饰的代码同时只能被一个线程执行,假如A线程在只能synchronized修饰的代码,B线程想执行这段代码只能等到A线程执行完之后才可以执行。那么有没有办法让线程A先“停下来”,让B线程去执行呢?
办法是有的,可以通过上述表格列出的Object类中的几个方法来实现。实现过程可以让A线程调用wait()方法,释放掉锁,让B线程获取所资源执行完后,可以使用notify()或notifyAll()方法来唤醒等待中的A线程,再让A线程去执行。
之前线程的执行顺序是随机的,那么现在我们就可以通过这三个方法来控制他们的执行顺序了,话不多说,一道经典的面试题:两个线程,一个打印5次A,另一个打印5次B,现在要让AB交替打印,如何实现(这里使用这三个方法实现):
实现代码如下
public class PrintAB {
// 该变量可以理解成:上一次打印是否是打印的字符 A。
private volatile boolean flag = false;
/**
* 打印字符 A 的方法
*/
private synchronized void printA(){
try {
// 判断上一次打印是否是打印的 A,如果是就进行等待,如果不是就执行下面的代码。
while (flag){
wait();
}
System.out.println("A");
flag = true;
// 唤醒在等待的线程
notifyAll();
}catch (InterruptedException e){
e.printStackTrace();
}
}
/**
* 打印字符 B 的方法
*/
private synchronized void printB(){
try{
// 判断上一次打印是否是打印的 B,如果是就进行等待,如果不是就执行下面的代码。
// 注意这里是去反,因为上次打印如果不是A,肯定就是B。
while (!flag){
wait();
}
System.out.println("B");
flag = false;
// 唤醒在等待的线程
notifyAll();
}catch (InterruptedException e){
e.printStackTrace();
}
}
public static void main(String[] args) {
PrintAB pAB = new PrintAB();
for(int i = 0; i < 10; i++) {
//打印A
new Thread(pAB::printA).start();
//打印B
new Thread(pAB::printB).start();
}
}
}
运行结果如下:
3.Lock锁
Java5新增加了Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步。下面是它的一些常用api
方法名 | 方法描述 |
---|---|
lock() | 获取锁,调用该方法当前线程会获取锁,当锁获取后,从该方法返回。 |
unlock() | 释放锁 |
tryLock() | 尝试非阻塞地获取锁,调用该方法后立即返回,如果能够获取锁则返回true,否则返回false。 |
tryLock(long timeout,TimeUnit unit) | 如果获取到锁,立即返回true,否则会等待参数给定的时间单元,在等待的过程中,如果获取到了锁,就返回true,如果等待超时则返回false。 |
lockInterruptibly() | 可中断地获取锁,与lock()方法的不同之处在于该方法会响应中断,即在锁的获取中可以中断当前线程。 |
newCondition() | 获取等待通知组件,在组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将会释放锁。 |
通过Lock锁也可以实现交替打印AB,代码演示如下:
public class PrintAB2 {
private Lock lock = new ReentrantLock();
// 创建三个对象 要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
private volatile boolean flag = false;
public void printA() {
lock.lock();
try {
if (flag) {
try {
conditionA.await();//A等待 造成当前线程在接到信号或被中断之前一直处于等待状态。
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("A");
conditionB.signal();//唤醒B 唤醒一个等待线程。
flag = true;//重新赋值
} finally {
lock.unlock();//A解锁 把资源给其他人使用
}
}
public void printB() {
lock.lock();
try {
if (!flag) {
try {
conditionB.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
conditionA.signal();
flag = false;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
PrintAB2 printAB2 = new PrintAB2();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
printAB2.printA();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
printAB2.printB();
}
}
}).start();
}
}
运行结果如下:
总结
下期分享死锁以及经典的生产者消费者问题。记得点赞关注哟!