JAVA多线程(JUC)基础学习

440 阅读3分钟

线程

通常在一个进程里面可以包含多个线程,但是一个进程里面至少有一个线程。线程可以利用进程拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,把线程作为独立运行和独立调度的基本单位

卖票

这里举例学习Java基础的时候基本都会尝试的卖票练习

//定义一个资源类
class Ticker {
    private int ticker = 30;
    public synchronized void sale(){
        if (ticker>0){
            ticker--;
            System.out.println(Thread.currentThread().getName()+":"+ticker);
        }
    }
}
public class test1 {
    /**
     * 线程操作资源类
     * 判断干活通知
     * 防止虚假唤醒
     * @param args
     */
    public static void main(String[] args) {

        Ticker ticker = new Ticker();

        //三个线程调用卖票方法
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    ticker.sale();
                }
            }
        },"A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    ticker.sale();
                }
            }
        },"B").start();
        //lambada方式调用
        new Thread(()->{
            for (int i = 0; i < 15; i++) {
                ticker.sale();
            }
        },"C").start();
    }
}

在运行项目后,是可以正常处理完所有操作,并且没有超卖重复卖等问题,这都要归功于synchronized

ReentrantLock

这种写法用到了java.util.concurrent的Lock类,其中里面的ReentrantLock则是实现了Lock接口,通过ReentrantLock也能实现synchronized关键字的效果,他们的区别是

  • synchronized 不需要用户去手动释放锁,synchronized 代码执行完后系统会自动让线程释放对锁的占用。
  • ReentrantLock 则需要用户去手动释放锁,如果没有手动释放锁,就可能导致死锁现象。一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。 基本的实现方法也比较简单
class Ticker {
    private int ticker = 30;
    Lock lock = new ReentrantLock();
    public  void sale(){
        lock.lock();
        try {
            if (ticker>0){
                ticker--;
                System.out.println(Thread.currentThread().getName()+":"+ticker);
            }
        }finally {
            lock.unlock();
        }
    }
}
  • Lock 是显式锁,synchronized 是隐式锁
  • Lock 只有代码块锁,而 synchronized 有代码块锁和方法锁
  • Lock 锁会让JVM花较少的时间调度线程,性能更好,子类多扩展性好
  • 优先顺序 Lock > synchronized

生产者-消费者

一般的解决思路

先提出一个题目

  • 现在两个线程,可以操作初始值为零的一个变量,实现一个线程对该变量加1,一个线程对该变量-1,实现交替,重复10轮,变量初始值为0。
  • 首先定义资源类的资源以及操作的方法
class Shop {
    private int cake = 0;

    public synchronized void add() throws InterruptedException {

        while (cake!=0){
            this.wait();
        }
        cake++;
        System.out.println(Thread.currentThread().getName()+":"+cake);
        this.notifyAll();
    }
    
    public synchronized void sub() throws InterruptedException {

        while (cake==0){
            this.wait();
        }
        cake--;
        System.out.println(Thread.currentThread().getName()+":"+cake);
        this.notifyAll();
    }
}

这里为什么在判断的时候用while而不是if,举个例子,加入有超过2个线程在操作这个资源类,例如有ABCD四个线程,AC进行add(),BD进行sub()。

  • 如果此时变量是0,那么B、D是同时在wait,A和C因为add方法被加了锁,所以只有一个方法进行add
  • notifyall之后,此时C线程也会进行add操作,因为此时他已经是在判断条件内部里面了
  • 如果使用if,则唤醒后没有继续判断number的情况,同理B,D线程也是。使用while的意义就是,让他重新进行一次判断

这里扩展一个知识点,wait方法和notify方法,查看API我们可以见到这两个方法属于Object的方法,wait 和 notify 必须要配合synchronized 关键字使用。

  • 编写线程,这里采用四个
public class _生产者消费者 {
    public static void main(String[] args) {
        Shop shop = new Shop();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.add();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.sub();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.add();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.sub();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }
}

运行之后,没有异常

使用JUC解决

即就是原先我们使用synchronized ,wait,notify,现在使用JUC的lock(ReentrantLock),condition,condition.await(), condition.signalAll();

  • 代码如下
/**
 * 新版写法
 */
class Shop2 {
    private int cake = 0;
    private Lock lock = new ReentrantLock();
    private Condition  condition = lock.newCondition();

    public void add() throws InterruptedException {
        lock.lock();
        try {
            while (cake!=0){
                condition.await();
            }
            cake++;
            System.out.println(Thread.currentThread().getName()+":"+cake);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }

    public void sub() throws InterruptedException {
        lock.lock();
        try {
            while (cake==0){
                condition.await();
            }
            cake--;
            System.out.println(Thread.currentThread().getName()+":"+cake);
            condition.signalAll();
        }finally {
            lock.unlock();
        }
    }

}

/**
 * 题目:现在两个线程,可以操作初始值为零的一个变量,
 *      实现一个线程对该变量加1,一个线程对该变量-1,
 *      实现交替,来10轮,变量初始值为0.
 *      1.高内聚低耦合前提下,线程操作资源类
 *      2.判断/干活/通知
 *      3.防止虚假唤醒(判断只能用while,不能用if)
 * 知识小总结:多线程编程套路+while判断+新版写法
 */
public class _生产者消费者2 {
    public static void main(String[] args) {
        Shop2 shop = new Shop2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.add();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"AA").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.sub();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"BB").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.add();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"CC").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10; i++) {
                        shop.sub();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"DD").start();
    }
}

那么为什么要使用JUC替代原来的方法呢?新技术的产生会替代就技术解决不了的事情,我们看看下面的例子

精准通知

多个线程之间的调用顺序,实现A->B->C:三个线程的启动顺序如下:AA打印5次,BB打印10次,CC打印15次,持续5轮。

  • 主方法
public class _精确通知顺序访问 {
    public static void main(String[] args) {
        PrintfDemo2 printfDemo = new PrintfDemo2();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                printfDemo.printf(1,5);
            }
        },"AA").start();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                printfDemo.printf(2,10);
            }
        },"BB").start();
        new Thread(()->{
            for (int i = 0; i < 5; i++) {
                printfDemo.printf(3,15);
            }
        },"CC").start();
    }
}
  • 资源类
class PrintfDemo2{
    private int num = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private Condition conditions[] = {condition1,condition2,condition3};

    public void printf(int signal,int count){
        lock.lock();
        try {
            while (num!=signal){
                conditions[signal-1].await();
            }
            for (int i = 1; i <= count; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            num=num%3+1;
            conditions[num-1].signal();
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}

另一种简单写法

class PrintfDemo{
    private int num = 1;
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();

    public void printf5(){
        lock.lock();
        try {
            //判断
            while (num!=1){
                condition1.await();
            }
            //操作
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
            }
            //通知
            num = 2;
            condition2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printf10(){
        lock.lock();
        try {
            //判断
            while (num!=2){
                condition2.await();
            }
            //操作
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
            }
            //通知
            num = 3;
            condition3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
    public void printf15(){
        lock.lock();
        try {
            //判断
            while (num!=3){
                condition3.await();
            }
            //操作
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName()+"打印"+i+"次");
            }
            //通知
            num = 1;
            condition1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

}

所以这就是对老技术的优化,精准通知,精准唤醒,通过对指定的condition唤醒来达到对指定的线程的唤醒。