JUC简介
JUC是java.util.concurrent
工具包的简称,这是一个处理线程的工具包,JDK1.5开始出现的。
线程与进程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位,是操作系统结构的基础。在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总得来说。进程,指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程是资源分配的最小单位。线程,系统分配处理器时间资源的基本单元,或者说进程之内独立执行的个单元执行流。线程程序执行的最小单位。
线程的状态
线程的状态包括NEW(新建)
、RUNNABLE(准备就绪)
、BLOCKED(阻塞)
、WAITING(不见不散)
、TIMED_WATING(过时不候)
、TERMINATED(终结)
。
wait/sleep
sleep
是Thread
的静态方法,wait
是Object
的方法,任意对象实例都能调用。sleep
不会释放锁,它也不需要占用锁。wait
会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized
中)。
它们都可以被 interrupted
方法中断。
并发和并行
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点。例子:春运抢票 电商秒杀…..
并行:多项工作一起执行,之后再汇总。例子:泡方便面,电水壶烧水,一边撕调料倒入桶中。
管程
是一种同步机制,保证同一个时间,只有一个线程访问被保护数据或者代码。
用户线程和守护线程
用户线程:自定义线程。主线程结束了,用户线程还在运行,JVM存活。
守护线程:比如垃圾回收。没有用户线程了,都是守护线程,JVM结束。
Lock接口
synchronized关键字
synchronized
是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号
{}
括起来的代码,作用的对象是调用这个代码块的对象; - 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 虽然可以使用
synchronized
来定义方法,但synchronized
并不属于方法定义的一部分,因此synchronized
关键字不能被继承。如果在父类中的某个方法使用了synchronized
关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized
关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同方法,因此,子类的方法也就相当于同步了。
- 虽然可以使用
- 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
- 修饰一个类,其作用的范围是
synchronized
后面括号括起来的部分,作用主的对象是这个类的所有对象。
下面用三个线程模拟卖票的例子
public class Ticket {
private int number = 30;
public synchronized void sale(){
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出票,"+"剩下:"+(--number));
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
}, "A").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
}, "B").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
}, "C").start();
}
}
创建线程的多种方式
- 继承
Thread
类 - 实现
Runnable
接口 - 使用
Callable
接口 - 使用线程池
Lock接口
Lock
实现提供了比使用synchronized
方法和语句可获得的更广泛的锁定操作。它允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock
接口有实现类ReentrantLock
、ReentrantReadWriteLock.ReadLock
、ReentrantReadWriteLock.WriteLock
。
Lock
与Synchronized
区别:
Lock
不是Java 语言内置的,synchronized
是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在 finally 块中释放锁;Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;- 通过
Lock
可以知道有没有成功获取锁,而synchronized
却无法办到。 Lock
可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock
的性能要远远优于synchronized
。
下面用Lock
接口实现刚才卖票的例子(可重入锁)
public class TicketLock {
private int number = 30;
private final ReentrantLock lock = new ReentrantLock();
public void sale(){
//上锁
lock.lock();
try {
if (number > 0){
System.out.println(Thread.currentThread().getName()+"卖出票,"+"剩下:"+(--number));
}
}finally {
//解锁
lock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
TicketLock ticket = new TicketLock();
new Thread(() -> {
for (int i=0;i<30;i++){
ticket.sale();
}
}, "A").start();
new Thread(() -> {
for (int i=0;i<30;i++){
ticket.sale();
}
}, "B").start();
new Thread(() -> {
for (int i=0;i<30;i++){
ticket.sale();
}
}, "C").start();
}
}
线程间通信
以一个加减1为例,先用synchronized
来实现
public class Share {
private int number = 0;
public synchronized void incr() throws InterruptedException {
while (number != 0){
wait();
}
System.out.println("值增加:"+(number++)+" ==> "+number);
notifyAll();
}
public synchronized void decr() throws InterruptedException {
while (number <= 0){
wait();
}
System.out.println("值减少:"+(number--)+" ==> "+number);
notifyAll();
}
}
public class ShareMain {
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();
}
}
}, "A").start();
new Thread(()->{
for (int i=0;i<10;i++){
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
}
}
结果是这种交替出现的样子
多线程编程步骤
- 创建资源类,在资源类创建属性和操作方法
- 在资源类操作方法
- 判断
- 干活
- 通知
- 创建多个线程,调用资源类的操作方法
- 防止虚假唤醒问题
线程间定制化通信
实现下面的场景,A打印5次,B打印10次,C打印15次,循环往复进行10轮。
public class OutLock {
/**
* 定义标志位
*/
private int flag = 1;
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
/**
* 打印A
*/
public void outA() throws InterruptedException {
lock.lock();
try {
while (flag != 1){
//等待
c1.await();
}
for (int i=0; i<5;i++){
System.out.println(Thread.currentThread().getName());
}
flag = 2;
//通知
c2.signal();
}finally {
lock.unlock();
}
}
/**
* 打印B
*/
public void outB() throws InterruptedException {
lock.lock();
try {
while (flag != 2){
//等待
c2.await();
}
for (int i=0; i<10;i++){
System.out.println(Thread.currentThread().getName());
}
flag = 3;
//通知
c3.signal();
}finally {
lock.unlock();
}
}
/**
* 打印C
*/
public void outC() throws InterruptedException {
lock.lock();
try {
while (flag != 3){
//等待
c3.await();
}
for (int i=0; i<15;i++){
System.out.println(Thread.currentThread().getName());
}
flag = 1;
//通知
c1.signal();
}finally {
lock.unlock();
}
}
}
public class OutLockMain {
public static void main(String[] args) {
OutLock outLock = new OutLock();
new Thread(() -> {
for (int i=0; i<10;i++){
try {
outLock.outA();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "A").start();
new Thread(() -> {
for (int i=0; i<10;i++){
try {
outLock.outB();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "B").start();
new Thread(() -> {
for (int i=0; i<10;i++){
try {
outLock.outC();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "C").start();
}
}
集合的线程安全
先演示一个集合的线程不安全例子
ArrayList<String> list = new ArrayList<>();
for (int i=0;i<30;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
会报出如下异常<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2961643/1646730775401-25e24b1f-7e74-4433-bb27-d04b1377d20c.png#clientId=u8f5376a5-8292-4&from=paste&height=249&id=u458a53de&margin=%5Bobject%20Object%5D&name=image.png&originHeight=249&originWidth=913&originalType=binary&ratio=1&size=32615&status=done&style=none&taskId=uf9f2991e-55d3-48ac-ae76-d43c6f651cd&width=913)<br />即`java.util.ConcurrentModificationException`,并发修改异常。<br />出现上述问题是因为`ArrayList`的`add()`方法没有同步,它是线程不安全的
Vector
Vector
是线程安全的
List<String> list = new Vector<>();
for (int i=0;i<30;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
但是使用Vector
效率较低
Collections
Collections
工具类中有_synchronizedList()_
方法,可以返回一个同步的List
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i=0;i<30;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
CopyOnWriteArrayList
使用JUC提供的类CopyOnWriteArrayList
,写时复制技术
List<String> list = new CopyOnWriteArrayList<>();
for (int i=0;i<30;i++){
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
`CopyOnWriteArrayList`采用并发读、独立写,最后再合并<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2961643/1646731669308-9f0dc7ea-8356-4f4c-b0f6-51b8d31dca08.png#clientId=u8f5376a5-8292-4&from=paste&height=275&id=ub8c4cca3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=275&originWidth=700&originalType=binary&ratio=1&size=69339&status=done&style=none&taskId=u62b08600-cad1-4002-8f76-2f6bb17db00&width=700)