JUC 并发编程
作者:pox21s
概述
在Java中,线程部分是一个重点,本篇文章说的是关于线程并发编程。JUC就是java.util .concurrent工具包的简称。这是一个处理线程的工具包,从JDK 1.5开始出现。
1.基本概念
1.1 进程和线程
1.1.1 定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个基本单位。
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、java虚拟机栈、本地方法栈等),但是同属一个进程的线程之间可以共享这个进程的所有资源。(只是进程含有的,线程自己的不能互相共享)
1.1.2 关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
1.1.3 区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
-
简而言之,一个程序至少有一个进程,一个进程至少有一个线程
-
线程的划分尺度小于进程,使得多线程程序的并发性高
-
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
-
线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。**但是线程不能够独立执行,**必须依存在应用程序中,由应用程序提供多个线程执行控制
-
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
1.1.4 线程和进程在使用上各有优缺点
线程执行开销小,但不利于资源的管理和保护;
而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
1.2 线程的状态
-
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
-
运行(RUNNABLE)
-
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该线程的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。
就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
-
就绪状态(RUNNABLE之READY)
-
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
-
调用线程的start()方法,此线程进入就绪状态。
-
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,其他线程也将进入就绪状态。
join()方法:
当前线程调用其他线程的join方法,当前线程进行阻塞状态,等待被调用线程执行完毕以后,结束阻塞状态,等待CPU时间片分配,当前线程才能继续执行。
t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程进入TIME_WAITING状态,当前线程不释放已经持有的对象锁。线程t执行完毕或者millis时间到,当前线程进入就绪状态。
-
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
yield 即 “谦让”,也是 Thread 类的方法。它让掉当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。
-
锁池里的线程拿到对象锁后,进入就绪状态。
所有等待获取锁的线程都会进入锁池,进入阻塞状态。
-
-
线程调度程序从可运行池中选择一个线程作为当前线程时,线程进入运行态。这也是线程进入运行状态的唯一的一种方式。
-
-
阻塞(BLOCKED)
表示线程阻塞于锁。
-
等待(WAITING)
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
-
超时等待(TIMED_WAITING)
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
sleep()方法就会使线程进入到超时等待状态,并且不会释放机锁。
-
终止(TERMINATED)
-
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
-
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程的start()方法只能被调用一次。
-
这6种状态定义在Thread类的State枚举中,可查看源码进行一一对应。
1.2.1 wait和sleep
sleep()
sleep() 方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。
因为sleep() 是static静态的方法(在synchronized前面加上static则说明锁的是这个类,单synchronized锁的是实例对象,sleep底层是通过本地方法实现),他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait()
wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池进入休眠,同时释放对象的机锁,使得其他线程能够访问这个对象,可以通过notify,notifyAll方法来唤醒等待的线程
notify,notifyAll方法并不会让当前线程进入休眠还会唤醒其他线程
1.2.1.1 区别
sleep会休眠但是会把锁握在手里,其他线程调用相同的对象,要等待锁,这时,锁还在sleep手中,其他线程则只有等待醒来并执行完方法后释放锁,才能执行方法。
但wait会休眠但是不会将锁握在手里,其他线程调用相同对象时可以直接执行方法,不用等待当前休眠的进程醒来。
1.3 并发和并行
1.3.1 概述
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
1.3.2 并发(一同出发,一起争夺一个资源)
与可以一起出发的并发(concurrent)相对的是不可以一起出发的顺序(sequential):
顺序:上一个开始执行的任务完成后,当前任务才能开始执行
并发:无论上一个开始执行的任务是否完成,当前任务都可以开始执行(也就是说,A B 顺序执行的话,A 一定会比 B 先完成,而并发执行则不一定。)
1.3.3 并行(一同出行)
与可以一起执行的并行(parallel)相对的是不可以一起执行的串行(serial):
串行:有一个任务执行单元,从物理上就只能一个任务、一个任务地执行
并行:有多个任务执行单元,从物理上就可以多个任务一起执行(也就是说,在任意时间点上,串行执行时必然只有一个任务在执行,而并行则不一定。)
综上,并发与并行并不是互斥的概念,只是前者关注的是任务的抽象调度、后者关注的是任务的实际执行。而它们又是相关的,比如并行一定会允许并发。
1.4 管程
一种监视器,俗称为锁,是一种同步机制,保证同一个时间,只能有一个线程访问被保护的数据或代码(块)等。
JVM的同步基于进入和退出,进入持有锁,退出释放锁,这个是使用管程对象来实现的。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是Java中实现管程技术的组成部分。
1.5 用户线程和守护线程
1.5.1 概述
用户线程:使用者自己创建出来的线程(new)
守护线程:守护线程 – 也称“服务线程”,在没有用户线程可服务时会自动离开,JVm创建,当JVM中没有用户线程时,JVM会自动关闭。
1.5.2 设置
通过setDaemon(true)可以将用户设置线程为“守护线程”;
1.5.3 区别
守护线程就是JVM中存在的线程,当没有用户线程可以服务的时候JVM就会结束守护线程。(JVM中只有守护线程时也会结束)。用户线程就是用户自己创建出来的线程,当有用户线程存在的时候,守护线程才能存在。
2. 创建线程的方式
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
2.1 Java可以用四种方式来创建线程
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
- 使用线程池例如用Executor框架
2.1.1 继承Thread类创建线程
通过在类名上继承Thread并重写其中的run()方法来实现,在创建实例的时候调用xxx.start()方法来启动线程
2.1.2 实现Runnable接口创建线程
实现Runnable接口并重写其中的run()方法,创建实例以后通过放入线程来创建线程
2.1.2.1 实例
public class Main {
public static void main(String[] args){
undefined
// 创建并启动线程,这里的myThread已经实现了Runnable接口并重写了run()方法
MyThread2 myThread=new MyThread2();
Thread thread=new Thread(myThread);
thread().start();
// 或者new Thread(new MyThread2()).start();
}
}
2.1.3 使用Callable和Future创建线程
实现Callable接口并重写其中的call()方法。和实现Runnable接口不同的是,Callable有返回值,并且需要配合FutureTask来接收返回值,Thread并不能直接接收Callable接口的实现类,只能接收FutureTask。
2.1.4 实现
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//实现Callable接口
public class CallableTest {
public static void main(String[] args) {
//执行Callable 方式,需要FutureTask 实现实现,用于接收运算结果
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
new Thread(futureTask).start();
//接收线程运算后的结果
try {
Integer sum = futureTask.get();
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
2.1.4 使用线程池例如用Executor框架
线程池提供了一个线程队列,队列中保存着所有等待状态的线程。避免了创建与销毁额外开销,提升了响应速度。实现了线程复用。
2.1.4.1 实现
Callable<Singleton4> callable = new Callable<Singleton4>() {
@Override
public Singleton4 call() throws Exception {
return Singleton4.getInstance();
}
};
// 创建一个线程池,并设定大小
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 通过future来接收线程启动后返回的结果
Future<Singleton4> f1 = threadPool.submit(callable);
Future<Singleton4> f2 = threadPool.submit(callable);
// 获取返回值
Singleton4 s6 = f1.get();
Singleton4 s7 = f1.get();
System.out.println(s6 == s7);
threadPool.shutdown();
}
3.多线程编程步骤
-
创建资源类,编写属性和操作方法
-
在资源类设置操作方法
- 判断
- 执行
- 通知
-
创建多个线程,调用资源类的操作方法
-
防止虚假唤醒,将在资源类操作方法的判断条件设置在while中
为防止虚假唤醒,不要使用if来进行线程状态的判定,而是通过while来进行判断,实时判断更新。
4. Synchronized
4.1 概述
Java自带的关键字,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。在发生异常的时候会自动释放锁。
由JVM来控制锁的开和关,用户无法控制。
4.2 实例
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员1").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员2").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员3").start();
}
}
class Ticket{
private int num = 30;
public synchronized void sale(){
if (num > 0 ){
// 获取当前执行的线程的名字
System.out.println(Thread.currentThread().getName()+"售卖第"+(num--)+"票"+"还剩下"+num+"票");
}
}
}
5.LOCK
5.1 概述
Lock不是java中的关键字,通过实例化来获取,可以让用户自己手动的上锁和解锁,如果没有设定解锁方式,在发生异常时不能正常解锁,则会发生死锁现象。
5.2 实例
import java.util.concurrent.locks.ReentrantLock;
public class SaleTicketByLockDemo {
public static void main(String[] args) {
SaleTicketByLock ticket = new SaleTicketByLock();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员1").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员2").start();
new Thread(() ->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"售票员3").start();
}
}
class SaleTicketByLock{
// 实现Lock
ReentrantLock lock =new ReentrantLock();
private int num = 30;
// 这里使用try finally 的方式来确保无论是否发生异常最后都能正确的关闭锁,以确保不会发生死锁的方式
public synchronized void sale(){
lock.lock();
try {
if (num > 0 ){
System.out.println(Thread.currentThread().getName()+"售卖第"+(num--)+"票"+"还剩下"+num+"票");
}
} finally {
lock.unlock();
}
}
}
6.Lock和Synchronized的区别
6.1 概述
6.2 区别
-
来源
lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现。
-
异常是否释放锁
synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
原因:synchronized底层会释放两次锁,第一次释放为正常释放,第二个为当出现异常时的释放
-
是否响应中断
lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
-
是否知道获取锁
Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
-
Lock可以提高多个线程进行读操作的效率
readwritelock就是实现读线程共享。
-
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
-
synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度
-
lock可以通过condition来达到精确唤醒
synchronized只能随机唤醒或者全部唤醒
6.3 volatile关键字
保证数据的可见性,能够保证一定的有序性,不能保证原子性,禁止指令重排
锁的强度:synchronized > lock > volatile ,推荐解决问题从底用到高
7.线程通信
7.1 用Synchronized实现
package syn;
/**
* @Author PoX21s
* @Date: 2021/11/1 17:01
* @Version 1.0
*/
public class ThreadDemo1 {
public static void main(String[] args) {
share share = new share();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程一").start();
new Thread(() ->{
for (int i = 0; i < 10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printS