juc--并发编程的核心问题总结①

文章中标明星号的地方说明是需要重点掌握的

一、juc基础知识

1. 什么是juc

java.util.concurrent (juc是包名的简写)在并发编程中使用的工具类,是关于并发编程的API。

2. 线程和进程

  • 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位。
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
  • 线程上下文切换比进程上下文切换要快得多。
  • 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源,某进程内的线程在其他进程不可见。
  • java默认线程有2个。(main线程、GC线程)

问:java可以开启线程吗?
答:不能!!只能通过本地方法进行调用
理由如下:在这里插入图片描述
进入start()方法:
在这里插入图片描述
start0是一个本地方法,底层是C++,java无法直接操作
在这里插入图片描述

3. 并发和并行的区别

并发:一个处理器同时处理多个任务。同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的。

并行:多个处理器或者是多核的处理器同时处理多个不同的任务。同一时刻,有多条指令在多个处理器上同时执行。

并发编程的本质:充分利用CPU的资源

4. 线程的六种状态

  1. NEW :线程刚创建完但未启动
  2. RUNNABLE :线程运行
  3. BLOCKED :线程阻塞
  4. WAITING :线程等待
  5. TIMED_WAITING :线程超时等待
  6. TERMINATED :线程执行完成

问:wait()和sleep()的区别

  • wait()来自Object类,sleep()来自Thread类。
  • wait()会释放锁,sleep()不会释放锁。
  • wait()只能在同步代码块中使用,sleep()可以在任何地方使用。
  • wait()不需要捕获异常,sleep()必须要捕获异常。
    在这里插入图片描述

二*、 Lock锁

Lock是一个接口。
在这里插入图片描述
在这里插入图片描述
公平锁:先来后到。加锁前先查看是否有排队等待的线程,有的话优先处理排在前面的线程。
非公平锁:线程加锁时直接尝试获取锁(线程间竞争锁),获取不到就自动到队尾等待。
默认非公平锁。
在这里插入图片描述
代码示例:
在这里插入图片描述

lock.lock() 最规范的写法是写到 try 语句的外面。
若要写在里面,请写到try语句块的第一行,防止try中加锁前的代码出现异常。
因为如果采用Lock,必须主动去释放锁。而且在发生异常时,也不会自动释放锁。

ReentrantLock和synchronized都是JDK提供的可重入的锁。

什么是可重入锁呢?

1. Synchronized和Lock锁的区别

  • Synchronized是java内置关键字,Lock是一个java类
  • Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁。
  • Synchronized会自动释放锁,Lock需要手动释放,若不释放会造成死锁。
  • 使用Synchronized时,一个线程1获得了锁,另一个尝试获取锁的线程2会等待线程1释放锁,若线程1阻塞了那线程2会一直等待。使用Lock锁时,线程2就不一定会一直等待下去。
//该处不会等待,获取不到锁并直接返回false
boolean isLocked = lock.tryLock();
//该处会在10秒时间内处于等待中
lock.tryLock(10, TimeUnit.SECONDS);
  • Synchronized是可重入锁,且是不可中断的非公平锁。(由于Synchronized是关键字所以不能修改);Lock也是可重入锁,可以判断锁的状态,可以设置是否为公平锁。

如果线程A正在执行锁中的代码,线程B正在等待获取该锁,由于等待时间过长,线程B不想等待了,我们可以让它自己中断自己或者在别的线程中中断它,这种就是可中断锁。

  • Synchronized适合锁少量的代码同步问题,Lock适合锁大量的同步代码。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

相关高频问题-----生产者消费者问题中出现的虚假唤醒问题,详见JUC—线程间定制化通信

2. Condition详解

Condition用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。
在这里插入图片描述

Condition依赖于Lock接口:

Condition condition = lock.newCondition();

调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock()之间才可以使用。

condition.await();//等待
condition.signalAll();//唤醒全部线程
condition.signal();//唤醒线程

在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()。传统线程的通信方式,Condition都可以实现。

Condition的优势在于他可以精准的通知和唤醒线程。
代码如下:juc版本的生产者消费者问题

package com.jess;

/**
 * @program: juc
 * @description: Condition
 * @author: Jess
 **/

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 实现:A执行完通知B执行
 *      B执行完通知C执行
 */
public class demo2 {
    
    
    public static void main(String[] args) {
    
    
        Resource resource = new Resource();

        new Thread(()->{
    
    
            for (int i = 0; i < 2; i++) {
    
    
                resource.printA();
            }
        },"A线程").start();

        new Thread(()->{
    
    
            for (int i = 0; i < 2; i++) {
    
    
                resource.printB();
            }
        },"B线程").start();

        new Thread(()->{
    
    
            for (int i = 0; i < 2; i++) {
    
    
                resource.printC();
            }
        },"C线程").start();
    }
}

/**
* 资源类 number=1,2,3 对应 A,B,C执行
*/
class Resource{
    
    
    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int number = 1;

    public void printA(){
    
    
        lock.lock();
        try{
    
    
            while(number !=1){
    
    
                condition1.await();//A线程等待
            }
            System.out.println("A线程执行中......");
            number = 2;
            //唤醒B线程
            condition2.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }

    public void printB(){
    
    
        lock.lock();
        try{
    
    
            while(number != 2){
    
    
                condition2.await();
            }
            System.out.println("B线程执行中......");
            number = 3;
            condition3.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }

    public void printC(){
    
    
        lock.lock();
        try{
    
    
            while(number != 3){
    
    
                condition3.await();
            }
            System.out.println("C线程执行中......");
            number = 1;
            condition1.signal();
        }catch (Exception e){
    
    
            e.printStackTrace();
        }finally {
    
    
            lock.unlock();
        }
    }
}

在这里插入图片描述

三*、八锁现象

八锁就是关于锁的八个问题

public class demo3 {
    
    
    public static void main(String[] args) {
    
    
        Fruit fruit = new Fruit();

        new Thread(()->{
    
    
            fruit.apple();
        },"A线程").start();

        //休息1s
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        new Thread(()->{
    
    
            fruit.banana();
        },"B线程").start();
    }
}

class Fruit{
    
    
    public synchronized void apple(){
    
    
        //休息4s
        try {
    
    
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("吃苹果");
    }

    public synchronized void banana(){
    
    
        System.out.println("吃香蕉");
    }
}

问题1.标准情况下,两个线程哪个先运行?
问题2.apple()方法延迟4s后,两个线程哪个先运行?
答:都是线程A先执行。因为有锁的存在。synchronized锁的对象是方法的调用者,所以banana()和apple()方法都是由fruit对象调用的,二者用的同一把锁,谁先拿到锁就是谁先执行。

//新增一个普通方法
public void grape(){
    
    
        System.out.println("吃葡萄");
}

 public static void main(String[] args) {
    
    
        Fruit fruit = new Fruit();

        new Thread(()->{
    
    
            fruit.apple();
        },"A线程").start();

        //休息1s
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        new Thread(()->{
    
    
            fruit.grape();
        },"B线程").start();
    }

问题3.在新增grape()方法后,A、B线程谁先执行?
答:B线程先执行,即输出“吃葡萄”。grape()方法没有加锁,不是同步方法,不受锁的影响。

//两个对象,分别调用两个同步方法banana()和apple()
public static void main(String[] args) {
    
    
        Fruit fruit1 = new Fruit();
        Fruit fruit2 = new Fruit();

        new Thread(()->{
    
    
            fruit1.apple();
        },"A线程").start();

        //休息1s
        try {
    
    
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        new Thread(()->{
    
    
            fruit2.banana();
        },"B线程").start();
    }

问题4.两个对象,分别调用两个同步方法banana()和apple(),哪一个先执行?
答:banana()先执行。synchronized锁的对象不同,有两把锁,所以按照时间顺序执行。(apple()方法延迟了4s)

//apple()和banana()为静态方法,由同一个对象fruit调用  
// fruit.apple();
// fruit.banana();
class Fruit{
    
    
    public static synchronized void apple(){
    
    
        //休息4s
        try {
    
    
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("吃苹果");
    }

    public static synchronized void banana(){
    
    
        System.out.println("吃香蕉");
    }
}

问题5.apple()和banana()为静态方法,哪一个线程先执行?
答:apple()先执行。static方法在类加载的时候就已经加载进内存了,锁的对象是类的class对象,Fruit类只有唯一的一个class对象,所以两个方法使用的同一把锁。

//两个对象调用两个静态方法
Fruit fruit1 = new Fruit();
Fruit fruit2 = new Fruit();
fruit1.apple();
fruit2.banana();

问题6.两个fruit对象调用apple()和banana()静态方法,哪一个线程先执行?
答:apple()先执行。此时两个fruit对象的class类模板只有一个,class锁是唯一的,所以多个对象使用的仍然是同一个class锁。

//apple()是静态同步方法,banana()是普通同步方法
 fruit.apple();
 fruit.banana();
 public static synchronized void apple(){
    
    }
 public synchronized void banana(){
    
    }

问题7.一个fruit对象调用方法,apple()是静态同步方法,banana()是普通同步方法,哪一个线程先执行?
答:banana()先执行。apple()方法的锁,锁的是class。banana()方法的锁,锁的是fruit对象。两个方法用的不是同一把锁。

//apple()是静态同步方法,banana()是普通同步方法
 fruit1.apple();
 fruit2.banana();
 public static synchronized void apple(){
    
    }
 public synchronized void banana(){
    
    }

问题8.两个fruit对象调用方法,apple()是静态同步方法,banana()是普通同步方法,哪一个线程先执行?
答:banana()先执行。apple()方法的锁,锁的是class。banana()方法的锁,锁的是fruit对象。两个方法用的不是同一把锁。

四*、集合类不安全

List在单线程的情况下是安全的,但是多线程呢?

public class demo4 {
    
    
    public static void main(String[] args) {
    
    
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(()->{
    
    
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

在这里插入图片描述
解决方案详见 JUC—集合的线程安全吗?

五、Callable(简单)

Callable接口类似于Runnable,因为他们都是为其实例可能由另一个线程执行的类设计的。然而Runnable不返回结果,也不能抛出被检查的异常。

Callable可以有返回值,可以抛出异常,与Runnable方法不同(call方法)
在这里插入图片描述

public class demo5 {
    
    
    public static void main(String[] args) {
    
    
        //new Thread(new Runnable()).start();
        //FutureTask是Runnable的一个实现类,FutureTask可以调用Callable
        //相当于new Thread(new FutureTask<String>(Callable)).start();

        MyCallable callable = new MyCallable();
        FutureTask futureTask = new FutureTask(callable);
        new Thread(futureTask).start();
         try {
    
    
            String result = (String)futureTask.get();//获取callable返回结果
            System.out.println(result);//jess
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
    }
}

class MyCallable implements Callable<String>{
    
    
    @Override
    public String call() throws Exception {
    
    
        return "jess";
    }
}

六*、常用辅助类

1. CountDownLatch

使一个线程等待其他线程各自执行完毕后再执行。

CountDownLatch是通过一个计数器来实现的(减法计数器),计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

public class demo6 {
    
    
    public static void main(String[] args) {
    
    
        //初始计数值为5
        CountDownLatch countDownLatch = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) {
    
    
            new Thread(()->{
    
    
                System.out.println(Thread.currentThread().getName() + "执行完毕...");
                //数量-1
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }

        //等待计数器归零,然后再向下执行
        try {
    
    
            countDownLatch.await();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }

        System.out.println("全部执行完毕!!!");
    }
}

2. CyclicBarrier

利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

public class demo7 {
    
    
    public static void main(String[] args) {
    
    
        //参数:(计数,线程)  当计数器为7时,调用指定线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
    
    
            System.out.println("线程执行成功!!!");
        });

        for (int i = 0; i < 7; i++) {
    
    
            //lambda表达式中拿不到for循环的变量i,{}中本质是new的一个类
            final int temp = i;
            new Thread(()->{
    
    
                System.out.println(Thread.currentThread().getName()+" 执行 "+temp);

                try {
    
    
                    cyclicBarrier.await();//等待计数器变成7
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这里插入图片描述
若计数器等待的计数值变成8,而线程数量仍然是7不变,那 “线程执行成功!!!” 将不会打印,因为计数器只有7。

3*. Semaphore

Semaphore(信号量):其内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。如果计数器值为0,则表示没有共享资源可以访问,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

public class demo8 {
    
    
    public static void main(String[] args) {
    
    
        //信号量计数器值为3 该共享资源只有3个
        Semaphore semaphore = new Semaphore(3);

        for (int i = 0; i < 5; i++) {
    
    
            new Thread(()->{
    
    
                try {
    
    
                    semaphore.acquire();//获取信号量
                    System.out.println(Thread.currentThread().getName()+"得到共享资源");
                    semaphore.release();//释放信号量
                    System.out.println(Thread.currentThread().getName()+"释放共享资源");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

在这里插入图片描述
下篇:juc–并发编程的核心问题总结②

文章思路来自学习视频

猜你喜欢

转载自blog.csdn.net/myjess/article/details/121315938