JUC并发编程基础篇(上)

1、JUC概述

1.1、进程与线程的概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

总结来说:

进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位

线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程时程序执行的最小单位

1.2、线程的六种状态

1))NEW:初始状态,线程被构建,但是还没有调用 start 方法;

2)RUNNABLED:运行状态,JAVA 线程把操作系统中的就绪和运行两种状态统一称为“运行中” ;

3)BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况 :

  • 等待阻塞:运行的线程执行了 Thread.sleep 、wait()、 join() 等方法JVM 会把当前线程设置为等待状态,当 sleep 结束、join 线程终止或者线程被唤醒后,该线程从等待状态进入到阻塞状态,重新抢占锁后进行线程恢复;

  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么jvm会把当前的线程放入到锁池中

  • 其他阻塞:发出了 I/O请求时,JVM 会把当前线程设置为阻塞状态,当 I/O处理完毕则线程恢复;

4)WAITING:等待状态,没有超时时间,要被其他线程或者有其它的中断操作;执行wait()、join()、LockSupport.park();

5)TIME_WAITING:超时等待状态,超时以后自动返回;

执行 Thread.sleep(long)、wait(long)、join(long)、LockSupport.park(long)、LockSupport.parkNanos(long)、LockSupport.parkUntil(long)

6)TERMINATED:终止状态,表示当前线程执行完毕 。
img

注意:

1)sleep、join、yield时并不释放对象锁资源,在wait操作时会释放对象资源,wait在被notify/notifyAll唤醒时,重新去抢夺获取对象锁资源。

2)sleep可以在任何地方使用,而wait,notify,notifyAll只能在同步控制方法或者同步控制块中使用。

3)调用obj.wait()会立即释放锁,以便其他线程可以执行notify(),但是notify()不会立刻立刻释放sycronized(obj)中的对象锁,必须要等notify()所在线程执行完sycronized(obj)同步块中的所有代码才会释放这把锁。然后供等待的线程来抢夺对象锁

1.3、sleep和wait方法的区别

  1. 来源:sleep()方法是Thread类的静态方法,而 wait()方法是object类的实例方法。这意味着所有Java对象都可以调用wait()方法,而只有 Thread类及其子类可以调用sleep()方法。
  2. 锁释放:当线程调用sleep()方法时,它不会释放已经持有的任何对象锁。因此,如果线程在调用sleep()之前获取了锁,其他线程将无法访问受该锁保护的资源,直到睡眠时间结束。而当线程调用wait()方法时,它会释放持有的对象锁,允许其他线程访问受锁保护的资源。
  3. 唤醒机制:sleep()方法在指定的时间(毫秒)后自动唤醒线程。而wait()方法需要依赖其他线程调用相同对象的
    notify()或notifyAll()方法来唤醒等待的线程。如果没有其他线程调用这些方法,调用wait()的线程将一直等待下去。
  4. 使用场景:sleep()方法通常用于让线程暂停执行一段时间,以便其他线程执行或等待某些条件成熟。例如,在轮询某一资源时,可以让线程每隔一段时间检查一次资源状态。而wait()方法通常用于线程间的协作,一个线程在等待某个条件满足时调用wait()进入等待状态,而另一个线程在条件满足时调用notify()或notifyAll()来唤醒等待的线程。

​ 举个例子,假设有两个线程A和B。线程A负责生产数据,线程B负责消费数据。当数据队列为空时,线程B需要等待线程A生产数据。这时,线程B可以调用wait()方法进入等待状态,并释放锁,以便线程A可以生产数据。当线程A生产完数据后,调用notify()或notifyAl1()方法唤醒线程B,线程B可以继续消费数据。

1.4、并发和并行

1、并行
并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行,所以无论从微观还是从宏观来看,二者都是一起执行的。

2、并发
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并发的关键是你有处理多个任务的能力,不一定要同时。

并行的关键是你有同时处理多个任务的能力。

1.5、用户线程和守护线程

在Java中,线程分为两种类型:用户线程和守护线程。
守护线程是一种特殊的线程,它在后台默默地完成一些系统性的服务,比如垃圾回收线程,JIT线程就可以理解为守护线程。这些线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,Java虚拟机也就退出了。守护线程并不会阻止Java虚拟机退出。
设置守护线程的方法是调用Thread对象的setDaemon(true)方法。需要注意的是,一定要在调用线程的starti)方法之前设置。这是一个简单的守护线程的例子:

public class MyThread {
    public static void main(String[] args) {
        //使用Lambda 表达式实现这个接口,创建 线程t1
        Thread t1 = new Thread(() -> {
            //判断是否是守护线程,(后台运行的)
            System.out.println(Thread.currentThread().getName() + "::" + Thread.currentThread().isDaemon());
            while (true) {
                //主线程结束,程序还在运行,jvm 没停止
            }
        }, "t1");
        // 如果把他设置为守护线程 ,主线程结束这个程序没有用户线程了,结束了
        t1.setDaemon(false);
        //启动线程
        t1.start();
        System.out.println(Thread.currentThread().getName() +"结束");
    }
}

2、Lock接口

2.1、Synchronized

​ 在并发编程中存在线程安全问题,主要原因有:1.存在共享数据 2.多线程共同操作共享数据。关键字synchronized可以保证在同一时刻,只有一个线程可以执行某个方法或某个代码块,同时synchronized可以保证一个线程的变化可见(可见性),即可以代替volatile。

复习可以参考【Java并发编程之深入理解】Synchronized的使用_synchronize三种用法

2.2、Lock接口

Lock 实现提供比使用 synchronized 方法和语句可以获得的更广泛的锁定操作。它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象 Condition 。

当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由 try-finally 或 try-catch 保护,以确保在必要时释放锁定。
Lock 实现提供了使用 synchronized 方法和语句的附加功能,通过提供非阻塞尝试来获取锁 tryLock(),尝试获取可被中断的锁 lockInterruptibly() ,以及尝试获取可以超时 tryLock(long, TimeUnit)

public class Ticket implements Runnable {
    //票的数量
    private int ticket = 100;
    private Object obj = new Object();
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            //synchronized (obj){//多个线程必须使用同一把锁.
            try {
                lock.lock();
                if (ticket <= 0) {
                    //卖完了
                    break;
                } else {
                    Thread.sleep(100);
                    ticket--;
                    System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            // }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket);
        Thread t2 = new Thread(ticket);
        Thread t3 = new Thread(ticket);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}

2.3、lock和synchronized的差异

  • synchronized是java关键字,内置,而lock不是内置,是一个类,可以实现同步访问且比 synchronized中的方法更加丰富。

  • synchronized不会手动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)

  • lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待

  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

  • Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)锁会出现死锁,需要在 finally 块中释放锁)

  • lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待。

  • 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到

  • Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)

3、线程间通信

线程间通信有两种实现方法:

关键字 synchronizedwait()/notify() 这两个方法一起使用可以实现等待/通知模式

Lock 接口中的 newContition() 方法返回 Condition 对象,Condition 类也可以实现等待/通知模式

3.1、synchronized 实现案例

// 创建一个资源类
class Share{
    // 设置临界资源
    private int number = 0;

    // 实现+1操作
    public synchronized void incr() throws InterruptedException {
        // 操作:判断、干活、通知
        if (number != 0) {
            // number不为0,等待
            // wait 有一个特点,在哪里睡,就在哪里醒
            this.wait();	
        }
        number++;
        System.out.print(Thread.currentThread().getName()+"::"+number);
        // 唤醒其他线程
        // 注意这里的通知是随机的,就是只能通知全部
        this.notifyAll();
    }

    // 实现-1操作
    public synchronized void decr() throws InterruptedException {
        // 操作:判断、干活、通知
        if (number != 1) {
            // number不为0,等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        this.notifyAll();
    }
}
public class InterThreadCommunication {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.incr();
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.decr();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();
    }
}

3.2 虚假唤醒问题

虚假唤醒主要出现在多线程中出现。

同样使用上述案例,现在有四个线程,分别为A,B,C,D,其中A,C线程做+1操作,B,D线程做-1操作,想要的结尾应该是A,C线程输出值为1,B,D线程输出值为0 。修改上述代码如下:

// 创建一个资源类
class Share{
    // 设置临界资源
    private int number = 0;

    // 实现+1操作
    public synchronized void incr() throws InterruptedException {
        // 操作:判断、干活、通知
        if (number != 0) {
            // number不为0,等待
            this.wait();
        }
        number++;
        System.out.print(Thread.currentThread().getName()+"::"+number+"--->");
        // 唤醒其他线程
        this.notifyAll();
    }

    // 实现-1操作
    public synchronized void decr() throws InterruptedException {
        // 操作:判断、干活、通知
        if (number != 1) {
            // number不为0,等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        this.notifyAll();
    }
}
public class InterThreadCommunication {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.incr();
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.decr();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.incr();
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"C").start();

            new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 100; i++) {
                        share.decr();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"D").start();
    }
}

结果如下

在这里插入图片描述

所谓虚假唤醒,就是 wait()方法的一个特点,总结来说 wait() 方法使线程在哪里睡就在哪里醒。 这是什么意思呢?那就以上述代码为例。

当 A 进入临界区,BCD三个线程在 if 判断后进入 wait() 等待,当A线程完成操作,此时 number 值为1,notifyAll() 会随机唤醒一个线程。

现在C被唤醒,由于 wait() 方法使线程在哪里睡就在哪里醒,所以接下来C在执行时不会再通过 if 判断而是直接+1,此时 number 就是2了。从而导致最后输出的结果和我们预想的不一致。

按照 JDK1.8 文档的提示,将资源类的 incr() 方法和 decr() 方法中的if语句改为循环语句,修改代码如下:

// 创建一个资源类
class Share{
    // 设置临界资源
    private int number = 0;

    // 实现+1操作
    public synchronized void incr() throws InterruptedException {
        // 操作:判断、干活、通知
        while (number != 0) {
            // number不为0,等待
            // 哪里睡哪里起
            this.wait();
        }
        number++;
        System.out.print(Thread.currentThread().getName()+"::"+number+"--->");
        // 唤醒其他线程
        this.notifyAll();
    }

    // 实现-1操作
    public synchronized void decr() throws InterruptedException {
        // 操作:判断、干活、通知
        while (number != 1) {
            // number不为0,等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName()+"::"+number);
        this.notifyAll();
    }
}

3.3、Lock实现

在 Lock 接口中,有一个 newCondition() 方法,该方法返回一个新 Condition 绑定到该实例 Lock 实例。

Condition 类中有 await() 和 signalAll() 等方法,和 synchronized 实现案例中的 wait() 和 notifyAll() 方法相同。所以通过 Lock 接口创建一个 Condition 对象,由该对象的方法进行等待和唤醒操作

class Share {
    // 设置临界资源
    private int number = 0;
    // 创建一个Com
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    // 实现+1操作
    public void incr() {
        // 上锁
        lock.lock();
        try {
            // 判断
            while (number != 0) {
                condition.await();
            }
            // 干活
            number++;
            System.out.print(Thread.currentThread().getName() + "::" + number + "--->");
            // 通知
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    // 实现-1操作
    public void decr() throws InterruptedException {
        // 上锁
        lock.lock();
        try {
            // 判断
            while (number != 1) {
                condition.await();
            }
            // 干活
            number--;
            System.out.println(Thread.currentThread().getName() + "::" + number);
            // 通知
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

3.4、线程定制化通信

案例实现

案列:启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次,一共进行10轮

具体思路:
每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志 位的线程

创建一个可重入锁 private Lock lock = new ReentrantLock();
分别创建三个开锁通知 private Condition c1 = lock.newCondition();(他们能实现指定唤醒)

(注意)具体资源类中的A线程代码操作
上锁,(执行具体操作(判断、操作、通知),解锁)放于try、finally,具体代码如下

class Share{
    private int flag = 1;

    private Lock lock = new ReentrantLock();
    // 创建三个Comdition对象,为了定向唤醒相乘
    Condition c1 = lock.newCondition();
    Condition c2 = lock.newCondition();
    Condition c3 = lock.newCondition();

    public void Aprint(int loop) {
        //上锁
        lock.lock();
        try{
            // 判断
            while(flag!=1) {
                c1.await();
            }
            // 干活
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
            }
            flag = 2; //修改标志位,定向唤醒 线程b
            // 唤醒
            c2.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    public void Bprint(int loop) {
        //上锁
        lock.lock();
        try{
            // 判断
            while(flag!=2) {
                c2.await();
            }
            // 干活
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
            }
            flag = 3; //修改标志位,定向唤醒 线程b
            // 唤醒
            c3.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    public void Cprint(int loop) {
        //上锁
        lock.lock();
        try{
            // 判断
            while(flag!=3) {
                c3.await();
            }
            // 干活
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " ::本次第" + i + "次打印,是第" + loop+ "次循环");
            }
            flag = 1; //修改标志位,定向唤醒 线程b
            // 唤醒
            c1.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 解锁
            lock.unlock();
        }
    }
}

public class CustomInterThreadCommunication {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    share.Aprint(i);
                }

            }
        },"A").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 10; i++) {
                    share.Bprint(i);
                }
            }
        },"B").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 20; i++) {
                    share.Cprint(i);
                }

            }
        },"C").start();
    }
}

注意点:

进程/线程同步有四个原则,都是为了禁止两个进程同时进入临界区。同步机制应该遵循以下原则

  • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区

  • 忙则等待:当已经有进程进入临界区的时候,其他试图进入临界区的进程必须等待

  • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区

  • 让权等待:当进程不能进入临界区的时候,应立即释放处理机,防止进程忙等待

    很显然,该案例被称为单标志法。因为该案例设置一个公用整型变量flag,用于指示被允许进入临界区的进程编号。

若 flag =1,则允许 P1进程进入临界区;若 flag =2,则允许 P2 进程进入临界区;若 flag =3,则允许 P3 进程进入临界区

该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也无法进入临界区比如,若 P3 顺利进入临界区并从临界区离开,则此时临界区是空闲的,但 P1并没有进入临界区的打算,flag = 1 一直成立,P3 就无法再次进入临界区。违背了"空闲让进"原则,让资源利用不充分·

比如,将上述代码中的 main() 方法的A、B线程都只循环十次而C线程循环20次 ,但其实C线程在循环10次以后就不能访问 Share 资源了,因为 A 线程已经不再访问同时 flag 值不再改变了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值