最近阅读了《Java高并发实战》一书,也了解了一些多线程方面的知识,但是一直没有尝试过写Coding。毕竟纸上得来终觉浅,因此通过本篇文章,对多个线程轮流打印、死锁、读写锁的实现问题进行总结,算是对多线程的一种巩固。主要涉及到的知识点就是synchronized
锁和wait
、notify
线程通信机制。
给定三个线程,代码的逻辑顺序是A->B->C
,每个线程内分别打印一条“This is x”
语句,如何做到最终打印顺序是C->B->A
。
要把多个线程的执行顺序给安排上,听起来就感觉要加锁,某个线程加锁执行完成,释放锁再进行下一个线程的执行,这只能保证可以以一定的顺序执行而不会乱序,但是具体的顺序就需要引入一个变量,用于标识当前应该由几号线程进行打印。在每个线程内去不断轮询是否轮到自己打印,如果没有轮到自己打印就wait进入阻塞状态,当别的线程打印完后,自己被唤醒继续去轮询是否轮到自己打印,当轮到自己时候,打印完成修改变量,并唤醒其它的所有线程,让其它的线程去判断是否轮到自己打印。整个逻辑就是如此,如果需要轮流打印C->B->A
多次,使用for
循环调用orderThread.printX
即可。
public class OrderThread {
private int orderNum = 3;
public static void main(String[] args) {
OrderThread orderThread = new OrderThread();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printA();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printB();
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
orderThread.printC();
}
});
t1.start();
t2.start();
t3.start();
}
public synchronized void printA(){
while (orderNum != 1) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 3;
System.out.println("This is A");
notifyAll();
}
public synchronized void printB() {
while (orderNum != 2) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 1;
System.out.println("This is B");
notifyAll();
}
public synchronized void printC() {
while (orderNum != 3) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 2;
System.out.println("This is C");
notifyAll();
}
}
This is C
This is B
This is A
给定三个线程,代码的逻辑顺序是A->B->C
,三个线程按照C->B->A
的顺序轮流打印1-100
。
该问题是“线程轮流打印”问题的拓展,依然是三个线程轮流打印,只是打印的内容进行了改变,需要从1开始计数打印。这就需要我们设置一个全局变量,每个线程内都对该全局变量进行打印并进行加一操作,以便下一个线程能够打印出正确的数字。需要注意的是:在while
循环和线程执行体内都需要对待打印的变量num
进行判断。while
循环中的num判断是为了多次调用print
方法,线程执行体内的num
判断是为了避免在调用print
方法时num
尚未达到100,但是print
方法执行过程中num
已经超出100的情况,如果取消掉线程执行体内的num
判断,程序会打印1-102。
public class ThreadPrint {
private int orderNum = 3;
public static int num = 1;
public static void main(String[] args) {
ThreadPrint orderThread = new ThreadPrint();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printA();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printB();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
while (num <= 100) {
orderThread.printC();
}
}
});
t1.start();
t2.start();
t3.start();
}
public synchronized void printA(){
while (orderNum != 1) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 3;
if (num <= 100) {
System.out.println("This is A " + num);
}
num++;
notifyAll();
}
public synchronized void printB() {
while (orderNum != 2) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 1;
if (num <= 100) {
System.out.println("This is B " + num);
}
num++;
notifyAll();
}
public synchronized void printC() {
while (orderNum != 3) {
try {
wait();
} catch (Exception e) {
e.printStackTrace();
}
}
orderNum = 2;
if (num <= 100) {
System.out.println("This is C " + num);
}
num++;
notifyAll();
}
}
给定两个线程,使得程序产生死锁。
既然需要产生死锁,就必须考虑到死锁产生的条件。
我们来逐条分析一下死锁的产生条件:
1、互斥,要做到资源互斥,只需要给资源加个锁,每个时刻就只能有一个线程能够获取到该资源;
2、不可剥夺,一般对于正常的程序而言,除非发生中断或者程序故障,否则对于线程已获得的资源,在未使用完成之前都是不可剥夺的,只能在使用后自己释放;
3、请求和保持,意味着当前线程需要持有一个资源,并去请求另一个资源;
4、循环等待,由上述条件进行引申,意味着A线程持有资源1并请求资源2,B线程持有资源2并请求资源1,形成环路。
综上,我们只需要创建两个不可剥夺的资源,并在不同的线程中通过synchronized
关键字,根据不同的顺序对资源进行持有,这样即可实现死锁。
public class DeadTest {
public static void main(String[] args) {
DeadLock deadLock1 = new DeadLock(true);
DeadLock deadLock2 = new DeadLock(false);
Thread t1 = new Thread(deadLock1);
Thread t2 = new Thread(deadLock2);
t1.start();
t2.start();
}
}
class DeadLock implements Runnable {
//用于标识两个线程
private boolean flag;
//两个资源
static final Object obj1 = new Object();
static final Object obj2 = new Object();
public DeadLock(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
while (true) {
//保持1并请求2
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有obj1");
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有obj2");
}
}
}
} else {
while (true) {
//保持2并请求1
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + "持有obj2");
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + "持有obj1");
}
}
}
}
}
}
实现一个读写锁。读锁可以在没有写锁时被多个线程同时持有,写锁是独占的,每次只能有一个写线程,但是可以有多个线程并发地读数据。
如果我们读写共用一把锁,那么实际上整个程序的读写就是串行的,同一时刻只能有一个线程对数据进行读或写,效率显然比较低,读写锁比互斥锁允许对于共享数据更大程度的并发。读写锁主要是为了能够将读写分离,因为程序其实是允许同一时刻,多个线程同时读取数据的,只要在读的期间没有写操作,即可保证所有读线程都能读到一致的数据。因此我们需要考虑到读写锁的原则:
1、读读能共存;
2、读写不能共存;
3、写写不能共存。
那么对于读锁而言,如果当前有线程在进行写入操作,则进入等待状态,直到所有的写锁释放即可获得读锁;对于写锁而言,只要当前没有写锁存在,则优先预抢占到写锁,避免被其它线程抢占,是一种先来先服务的实现,但是此时还没有真正获得写锁,直到判断当前不存在读锁,才能真正获取到写锁进行数据的写入。无论对于读锁还是写锁而言,锁的释放直接对锁资源进行更新,然后通知其它所有线程即可。
public class ReadWriteLockTest {
public static void main(String[] args) {
ReadWriteLockDemo readWriteLock = new ReadWriteLockDemo();
//启动写线程
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.write(1);
}
}, "Write1").start();
//启动10个读线程
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.read();
}
}).start();
}
//启动写线程
new Thread(new Runnable() {
@Override
public void run() {
readWriteLock.write(2);
}
}, "Write2").start();
}
}
class ReadWriteLockDemo {
private ReadWriteLock readWriteLock = new ReadWriteLock();
private int num = 0; //共享资源
//读
public void read() {
try {
readWriteLock.lockRead();
System.out.println(Thread.currentThread().getName() + " read " + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.unlockRead();
}
}
//写
public void write(int number) {
try {
readWriteLock.lockWrite();
this.num = number;
System.out.println(Thread.currentThread().getName() + " write " + num);
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.unlockWrite();
}
}
}
class ReadWriteLock {
private int readLock = 0;
private int writeLock = 0;
public synchronized void lockRead() throws Exception{
while (writeLock > 0) {
wait();
}
readLock++;
}
public synchronized void unlockRead() {
readLock--;
notifyAll();
}
public synchronized void lockWrite() throws Exception{
while (writeLock > 0) {
wait();
}
writeLock++;
while (readLock > 0) {
wait();
}
}
public synchronized void unlockWrite() {
writeLock--;
notifyAll();
}
}
Write1 write 1
Thread-1 read 1
Thread-2 read 1
Thread-5 read 1
Thread-0 read 1
Thread-3 read 1
Thread-4 read 1
Thread-8 read 1
Thread-6 read 1
Thread-9 read 1
Thread-7 read 1
Write2 write 2
从程序的运行结果中可以看出,对于读操作而言,只有在写操作结束后才能进行,并且不能保证线程执行的先后顺序,因为读操作并不是互斥的;而对于写操作而言,在读锁和写锁全部释放之后才能进行。因此,Read
线程必须在Write1
线程完成并释放写锁后才能执行,并且所有Read
线程的执行是无序的,Write2
线程只能等待所有Read
线程完成并释放读锁后才能执行。