多线程问题——轮流打印、死锁、读写锁实现

概述

最近阅读了《Java高并发实战》一书,也了解了一些多线程方面的知识,但是一直没有尝试过写Coding。毕竟纸上得来终觉浅,因此通过本篇文章,对多个线程轮流打印、死锁、读写锁的实现问题进行总结,算是对多线程的一种巩固。主要涉及到的知识点就是synchronized锁和waitnotify线程通信机制。

线程轮流打印

问题描述

给定三个线程,代码的逻辑顺序是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线程完成并释放读锁后才能执行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章