多线程
一、进程的概念
1.1 程序
程序是在系统中安装的一系列的文件,有特定的作用。是静止的内容。
1.2 进程
当打开某个程序的文件夹,双击程序的启动文件(一般windows上该文件的后缀名为exe),此时会启动该程序,启动后会打开窗口或者在托盘中显示相应图标。
此时打开进程窗口,会看到该程序对应的进程。
一般情况下,一个程序启动后会产生一个进程。正在运行的程序称为进程。
注意:有可能一个程序运行后产生多个进程。
注意:单核CPU在任何时间点上只能运行一个进程,微观串行,宏观并行。
串行和并行:
串行:一个一个的进行。
并行:多个一起进行。
目前绝大部分系统都是抢占式执行。即抢占CPU的执行时间。
二、线程的概念
线程是一个轻量级的进程,也可以说线程是一个弱化版的进程。一个进程中可以包含多个线程,称为多线程。
多个线程之间一样是微观串行,宏观并行。抢占CPU的执行时间,谁抢到谁执行。
注意: 如果是多核CPU有可能并行。并非一个线程执行完毕后再执行另一个线程,是交替执行,才能达到宏观并行。
Main线程:也成为主线程,当启动Java程序,使用main方法时,会创建一个main线程。
一个进程至少有一个线程(主线程),也可以有多个线程。
三、进程与线程的区别
- 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位
- 一个程序一般有一个进程,一个进行至少有一个线程,可以有多个线程。
- 进程之间一般不能共享数据,但是线程之间可以共享数据。
四、线程的组成
CPU的时间片,线程执行必须抢占时间片才能运行。
运行数据:
- 栈空间:运行过程中使用的局部变量,每个线程都有独立的栈空间。
- 堆空间:运行过程中使用的对象,多个线程可以共享对象
逻辑代码
五、线程的创建和启动
5.1 线程的创建
线程的创建常用两种方式:继承Thread类和实现Runnable接口。
5.1.1 继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
// currentThread()用来获取当前线程
// 得到当前线程名称
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=======" + i);
}
}
}
5.1.2 实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
// currentThread()用来获取当前线程
// 得到当前线程名称
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + ">>>>>>>" + i);
}
}
}
继承Thread类和实现Runnable接口区别?
- 继承Thread类就不能继承其他类,实现Runnable接口还可以继承其他类,有更好的扩展性。
- 继承Thread类的对象可以直接启动,而实现Runnable接口其实仅仅是重写了线程执行过程中的业务逻辑代码,要想启动线程还需要创建一个线程对象。
5.2 线程的启动
线程的启动需要使用start()方法。
public class TestMain {
public static void main(String[] args) {
// 创建继承自Thread类的对象
MyThread t1 = new MyThread();
t1.start();
// 创建实现了Runnable接口的对象
MyRunnable r = new MyRunnable();
Thread t2 = new Thread(r);
t2.start();
}
}
多线程交替执行,理论上来说会比单线程性能有提升,但是如果有共享数据会有线程安全的风险。
多线程是利用CPU的空闲时间执行,并非越多越好,还是要考虑CPU的性能。
start方法和run方法的区别?
- run方法中写的是线程执行的业务逻辑代码,如果直接调用run方法,会将业务逻辑在当前线程中执行,并不会开启多线程。
- start方法,表示开启多线程,进入线程的执行过程,抢占CPU执行时间,并执行run方法中的业务逻辑。
五、线程的基本状态
基本:
- 新建状态(初始状态):使用new关键字创建线程。
- 就绪状态:调用start方法后进入就绪状态,该状态可以开始抢占时间片。
- 运行状态:当抢占到时间片,进入运行状态,执行业务。当时间片到期,如果业务没有执行完毕,进入就绪状态。
- 终止状态:业务执行完毕,或者main执行完毕,进入终止状态,释放时间片。
注意:调用start方法后,线程只是进入了就绪状态,并没有立即执行。
六、线程执行过程中的常用方法
6.1 sleep
休眠。不会抢占时间片,直到休眠时间结束。时间结束后进入就绪状态。
public class MyThread extends Thread{
@Override
public void run() {
// currentThread()用来获取当前线程
// 得到当前线程名称
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=======" + i);
if(i == 30) {
try {
// 单位:毫秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
6.2 yield
放弃当前时间片,回到就绪状态,进入下次的时间片竞争。
public class MyThread extends Thread{
@Override
public void run() {
// currentThread()用来获取当前线程
// 得到当前线程名称
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=======" + i);
if(i == 30) {
Thread.yield();
}
}
}
}
6.3 join
将其他的线程合并到当前线程中。(插队)
public class TestMain1 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=======" + i);
}
}
};
Thread t1 = new Thread(r1);
Runnable r2 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + ">>>>>>" + i);
if(i == 30) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
}
应用场景:多线程下载,所有线程下载完毕才能合成一个下载内容,可以在合成的代码之前将其他线程都合并进来,以实现所有线程下载完毕后才执行合成。
七、线程的状态(等待)
等待:
- sleep方法调用后,线程进入限期等待,当时间到期后,线程进入就绪状态。
- join方法调用后,线程进入无限期等待,当加入的线程执行完毕后,当前线程才进入就绪状态。
- wait方法调用后,线程会进入无限期(限期)等待,直到被notify或notifyAll唤醒(超时到期)后,线程才会进入就绪状态。
八、线程安全
当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
// 售票员卖票问题
public class Seller extends Thread{
private static int ticket = 10;
private String name;
public Seller(String name) {
super();
this.name = name;
}
@Override
public void run() {
while(ticket > 0) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ticket > 0) {
ticket--;
System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
}
}
}
}
public class TestMain2 {
public static void main(String[] args) {
Seller s1 = new Seller("龙修");
s1.start();
Seller s2 = new Seller("文达");
s2.start();
Seller s3 = new Seller("国栋");
s3.start();
}
}
九、线程同步
同步:其他线程等待当前线程执行。
异步:多个线程同时执行。
将操作共享数据改变时的代码进行加锁,加锁后此段代码执行时,其他线程如果执行到此代码时需要等待。
语法:
同步代码块:
synchronized(加锁的对象){
// 临界资源修改的代码
}
public class Seller extends Thread{
private static Integer ticket = 10;
private String name;
public Seller(String name) {
super();
this.name = name;
}
@Override
public void run() {
while(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ticket) {
if(ticket > 0) {
ticket--;
System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
}
}
}
}
}
同步方法:
语法:
public synchronized void method(){
}
public synchronized static void method(){
}
同步方法与同步代码块的区别:
- 同步代码块比较灵活,只锁部分代码。而同步方法锁整个方法。
- 实例方法锁的是this对象
- 静态方法锁的是当前类.class
十、线程的状态(阻塞)
阻塞状态:当线程在执行过程中,执行到线程同步代码时,需要持有锁,此时,只会有一个线程持有锁,其他没有持有锁的线程被迫进入等待抢锁,称为阻塞状态。
十一、线程的通信
使用wait和notify\notifyAll方法来在多个线程之间进行通信。
wait():进入等待直到被唤醒
wait(long timeout):进入等待,设置超时时间,如果在没有到时间时,可以被唤醒,如果时间到还没被唤醒,会自动醒来。
notify():唤醒被wait的线程,如果有多个线程被同一个对象wait,一次notify只会随机唤醒一个线程。
notifyAll():唤醒所有被一个对象wait的线程。
注意:使用一个对象的wait或者notify方法,必须在该对象加锁的代码中。
public class Test {
private static final Object obj = new Object();
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=====" + i);
if(i == 30) {
synchronized (obj) {
try {
// 单位:毫秒,在2000毫秒内可以唤醒,如果没有唤醒,会自动醒来
// obj.wait(2000);
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Thread t = new Thread(r1);
t.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 300) {
synchronized (obj) {
// 每次notify只会随机唤醒一个
// notifyAll才会唤醒所有的
// obj.notify();
// obj.notify();
obj.notifyAll();
}
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
经典面试题:sleep和wait的区别?
- sleep是限期等待,只能等待时间到期才会进入就绪状态。wait如果不设置过期时间,只能等待被唤醒,如果设置过期时间,可以在时间内被唤醒,也可以等到时间超时自动醒来。
- wait会释放锁,sleep不释放锁。
sleep不释放锁的案例:
public class Test2 {
private static final Object obj = new Object();
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "=====" + i);
if(i == 30) {
synchronized (obj) {
try {
// 会持有锁
//Thread.sleep(10000);
// 会释放锁
obj.wait(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 200) {
synchronized (obj) {
System.out.println(">>>>>>>====" + i);
}
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
十二、死锁
当A线程持有a锁,并且需求b锁,而B线程持有b锁,并需求a锁,且都不释放各自的锁时,产生死锁。
public class Test3 {
public static void main(String[] args) {
Boy boy = new Boy();
Girl girl = new Girl();
boy.start();
girl.start();
}
private static class Boy extends Thread{
@Override
public void run() {
synchronized (a) {
System.out.println("男生抢到a");
// try {
// a.wait();
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (b) {
System.out.println("男生抢到b");
System.out.println("男生吃东西");
}
}
}
}
private static class Girl extends Thread{
@Override
public void run() {
synchronized (b) {
System.out.println("女生抢到b");
synchronized (a) {
System.out.println("女生抢到a");
System.out.println("女生吃东西");
// a.notify();
}
}
}
}
// 两根筷子
public static final Object a = new Object();
public static final Object b = new Object();
}
十三、生产消费模式
13.1 设计模式
解决某一类业务问题,经过实践证明,行之有效的经验的总结,称为设计模式。
90年代初,4人组总结并推出书。23种设计模式,将当时软件行业遇到问题类型分为3大类,共23种。
创建型模式:创建对象过程中使用的模式。一共5种,例如:单例模式、工厂模式、原型模式等。
结构型模式:多个对象共同形成一种新的结构。一共7种,例如:门面模式(装饰模式)、桥接模式、适配器模式。
行为型模式:多个对象之间产生行为。一共11种,例如:命令模式、监视模式、迭代模式、调停模式等。
13.2 生产消费模式
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
public class Car {
private String name;
public Car(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Car [name=" + name + "]";
}
}
public class Producer extends Thread{
private Storage storage;
public Producer(Storage storage) {
super();
this.storage = storage;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 15; i++) {
Car car = new Car(name + "===" + i);
storage.add(car);
}
}
}
public class Customer extends Thread{
private Storage storage;
public Customer(Storage storage) {
super();
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
storage.out();
}
}
}
public class Storage {
private Car [] cars = new Car[6];
private int size = 0;
public synchronized void add(Car car) {
while(size >= 6) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
cars[size++] = car;
System.out.println("添加了一辆汽车," + car);
this.notifyAll();
}
public synchronized void out() {
while(size <= 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Car car = cars[--size];
System.out.println("消费了一辆汽车," + car);
cars[size] = null;
this.notifyAll();
}
}
十四、线程的中止
让线程中止的方式:
- stop(不推荐使用),会导致程序出现未知的问题,数据没有及时保存,线程不正常退出。
- 设置标识符,在程序的执行过程中,通过判断标识符来确定是否停止。让线程在不满足条件下正常结束。
- interrupt 系统提供的标识。
14.1 stop
public class Test4 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; ; i++) {
System.out.println(name + "=====" + i);
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 200) {
t1.stop();
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
14.2 设置标识符
下面的案例,能够演示出效果,但是有瑕疵。
public class Test4 {
public static boolean b = false;
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; ; i++) {
System.out.println(name + "=====" + i);
if(b) {
break;
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 200) {
b = true;
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
14.3 interrupt
public class Test4 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; ; i++) {
System.out.println(name + "=====" + i);
// 判断是否被打断
if(Thread.interrupted()) {
break;
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 200) {
// 打断上面线程的执行
t1.interrupt();
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
当线程正在sleep或者wait时,打断该线程会报错。
public class Test4 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; ; i++) {
System.out.println(name + "=====" + i);
if(i == 30) {
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
// 当sleep或者wait时,遇到被打断,说明需要中止线程
// 那么也应该中止
break;
}
}
if(Thread.interrupted()) {
break;
}
}
}
};
Thread t1 = new Thread(r1);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 200) {
t1.interrupt();
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
十五、守护线程
守护线程的作用是守护其他线程执行,如果其他线程都结束了,守护线程会自动结束。JVM也是一个守护线程。
public class Test4 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; ; i++) {
System.out.println(name + "=====" + i);
}
}
};
Thread t1 = new Thread(r1);
// 设置为守护线程
t1.setDaemon(true);
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
}
十六、线程的优先级
可以设置线程的抢占CPU的时间片的优先程度,默认为5,最高为10,最低为1。
public class Test5 {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 200; i++) {
System.out.println(name + "=====" + i);
}
}
};
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(">>>>>>>" + i);
}
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.setPriority(Thread.MAX_PRIORITY); // 10
t2.setPriority(Thread.MIN_PRIORITY); // 1
t1.start();
t2.start();
}
}
十七、volatile关键字用法
volatile可以用来修饰属性,表示该属性具备有可见性,即多线程操作时,每次要读取该属性,必须加载它最新的变化。
public class Test6 {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
t1.start();
Runnable r2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 302; i++) {
System.out.println(">>>>>>>" + i);
if(i == 300) {
t1.a = "b";
}
}
}
};
Thread t2 = new Thread(r2);
t2.start();
}
private static class MyThread1 extends Thread{
public volatile String a = "a";
@Override
public void run() {
System.out.println("程序开始执行");
while(a.equals("a")) {
}
System.out.println("程序执行完毕");
}
}
}
上面的案例,当没有使用volatile关键字时,即使属性a的值被另一个线程改变了,由于当前线程没有空闲时间,无法做到最基本的值同步,所以,线程不会结束。
但是如果在while循环中加入一个打印输出的代码,就可以执行完毕,因为打印输出对于CPU来说有足够的空闲时间,而线程会在有足够空闲时间的情况下,尽量保证数据的一致性。
当使用volatile关键字时,表示每次读取属性a的值时,都会加载最新的变化。
经典面试题:volatile关键字的作用,以及与synchronized的区别?
- volatile保证属性的可见性,但是并不能保证线程安全,因为不能保证属性的互斥性。
- synchronized实现了线程安全,保证了属性的互斥性和可见性。
- 互斥性是指如果一个线程在操作,其他的线程需要等待,不能操作。
- 可见性是指如果一个线程改变了属性的值,其他线程在访问时需要去加载最新的变化。
十八、线程池
池本质是一个集合,用来存放和管理多个对象,避免频繁创建和销毁对象。
线程池就是管理线程的一个池。
池的管理,可以设置大小,最低的数量等。
将任务(业务)提交给线程池,由线程池分配分配线程和任务的执行,如果线程用完,任务会等待,直到有空闲的线程来执行该任务。
public class TestPool {
public static void main(String[] args) {
// 创建有4个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(4);
// 创建无上限的线程池,理论最大值为Integer的最大值
// ExecutorService service = Executors.newCachedThreadPool();
// 定义一个任务
Runnable r = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 100; i++) {
System.out.println(name + "----" + i);
}
}
};
// 将任务交给线程池去执行
service.submit(r);
service.submit(r);
service.submit(r);
service.submit(r);
service.submit(r);
service.submit(r);
// 关闭线程池
service.shutdown();
}
}
十九、Callable接口和Future接口
Callable接口类似Runnable接口,也能够处理业务,但是其有返回值。
Future接口用来接收Callable接口的返回值,使用get方法获取返回值,get方法是同步的,必须等待线程执行完毕才能获取结果,获取过程中,可能会因为线程执行异常导致获取失败。
案例:通过两个线程分别计算1
50和51100的和,然后计算总和。
public class TestPool2 {
public static void main(String[] args) {
// 创建有4个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(4);
// 创建无上限的线程池,理论最大值为Integer的最大值
// ExecutorService service = Executors.newCachedThreadPool();
// 求1~50之和
Callable<Integer> r1 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 50; i++) {
sum += i;
Thread.sleep(100);
}
System.out.println("1~50之和为:" + sum);
return sum;
}
};
// 求51~100之和
Callable<Integer> r2 = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 51; i <= 100; i++) {
sum += i;
}
System.out.println("51~100之和为:" + sum);
return sum;
}
};
// 将任务交给线程池去执行
Future<Integer> f1 = service.submit(r1);
Future<Integer> f2 =service.submit(r2);
// 通过get方法获取结果
try {
// 可能没有计算出结果,所以需要处理异常
System.out.println("结果为:" + (f1.get() + f2.get()));
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 关闭线程池
service.shutdown();
}
}
二十、Lock锁
20.1 Lock接口
在JDK1.5加入,与synchronized作用相似,但是lock接口更灵活,可以手动控制加锁和解锁,甚至可以尝试是否加锁。
注意:解锁的代码最好写在finally中。
20.2 重入锁(公平锁)
当创建锁对象时,使用无参构造或者在构造时传入false,表示使用重入锁,不公平,默认。
当创建锁对象时,使用构造时传入true,表示使用公平锁。公平是指先等待获取锁的线程会在其他线程释放锁时优先持有锁。在特殊的业务场景使用,保证公平,但会消耗更多的性能。
public class Seller extends Thread{
private static Integer ticket = 10;
private static ReentrantLock lock = new ReentrantLock();
private String name;
public Seller(String name) {
super();
this.name = name;
}
@Override
public void run() {
while(ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
// 加锁
lock.lock();
if(ticket > 0) {
ticket--;
System.out.println(name + "卖出一张票, 还剩下"+ticket+"张票");
}
} finally {
// 解锁
lock.unlock();
}
}
}
}
public class TestMain2 {
public static void main(String[] args) {
Seller s1 = new Seller("张三");
s1.start();
Seller s2 = new Seller("李四");
s2.start();
Seller s3 = new Seller("王五");
s3.start();
}
}
二十一、读写锁
普通的锁会不管读写,都会互斥,而如果所有的线程都在读时,其实没有线程安全的问题,当读操作远大于写操作时,可以使用读写锁,读写锁的特点是:只有线程有写时,才会互斥,如果都是读,会一起执行。
public class MyClass {
private int value;
// 普通重入锁
// private static final ReentrantLock lock = new ReentrantLock();
// 读写锁
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReadLock readLock = lock.readLock();
private static final WriteLock writeLock = lock.writeLock();
public int getValue() {
// lock.lock();
readLock.lock();
try {
Thread.sleep(1000);
return value;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
readLock.unlock();
}
return 0;
}
public void setValue(int value) {
// lock.lock();
writeLock.lock();
try {
Thread.sleep(1000);
this.value = value;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// lock.unlock();
writeLock.unlock();
}
}
}
public class TestMain {
public static void main(String[] args) {
final MyClass c = new MyClass();
// 定义读任务
Runnable r = new Runnable() {
@Override
public void run() {
c.getValue();
}
};
// 定义写任务
Runnable w = new Runnable() {
@Override
public void run() {
c.setValue(1);
}
};
// 创建线程池
ExecutorService service = Executors.newFixedThreadPool(20);
// 得到线程开始执行的系统时间
long startTime = System.currentTimeMillis();
// 加入2个写线程
for (int i = 0; i < 2; i++) {
service.submit(w);
}
// 加入18个读线程
for (int i = 0; i < 18; i++) {
service.submit(r);
}
// 关闭线程池
service.shutdown();
// 当线程池内的任务没结束时,会一直卡住
while(!service.isTerminated()) {
}
// 得到线程执行结束的系统时间
long endTime = System.currentTimeMillis();
System.out.println("一共使用了"+(endTime-startTime)+"毫秒");
}
}
二十二、线程安全的集合
22.1 Collections类中提供的线程安全集合的处理方法
静态方法:synchronizedXxx() 重写对应接口的方法,然后在方法中加锁后调用原来的方法。
在JDK1.2添加,性能与老版本的Vector之类的没有提升,只是接口统一而已。
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex),
mutex);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
synchronized (mutex) {list.replaceAll(operator);}
}
@Override
public void sort(Comparator<? super E> c) {
synchronized (mutex) {list.sort(c);}
}
/**
* SynchronizedRandomAccessList instances are serialized as
* SynchronizedList instances to allow them to be deserialized
* in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
* This method inverts the transformation. As a beneficial
* side-effect, it also grafts the RandomAccess marker onto
* SynchronizedList instances that were serialized in pre-1.4 JREs.
*
* Note: Unfortunately, SynchronizedRandomAccessList instances
* serialized in 1.4.1 and deserialized in 1.4 will become
* SynchronizedList instances, as this method was missing in 1.4.
*/
private Object readResolve() {
return (list instanceof RandomAccess
? new SynchronizedRandomAccessList<>(list)
: this);
}
}
22.2 CopyOnWriteArrayList
线程安全的ArrayList,操作与ArrayList相同。
加强版的读写锁,写有锁,读无锁。读写不阻塞,优于读写锁。
原理:写入时,会复制一个副本,在副本中写入,写入完成后,替换原来的地址。
特点:性能高,但是消耗额外的空间。
22.3 CopyOnWriteArraySet
线程安全的Set,底层以CopyOnWriteArrayList实现。
实现元素不重复的方式:每次复制副本后,使用addIfAbsent()方法添加元素,先遍历判断是否存在重复,若存在,则扔掉副本。
22.4 ConcurrentHashMap
JDK1.7中使用分段锁。
- 初始容量默认16段。
- 不对整个Map加锁,而是对16段分别加锁,即16把锁。
- 当多线程同时操作同一个段(例如添加元素)时,才会加锁互斥。而操作不同段时,由于是不同的锁,所以不互斥。
- 最理想的状态,是16个线程同时操作16段,可以不互斥执行。
JDK1.8后使用CAS(比较交换算法)
- 修改的方法包含三个核心参数,V、E、N
- V是要更新的变量,E是预期值,N是新值
- 只有当V等于E时,才将N赋值给V。否则表示被更新过,取消当次操作。
并行和并发:
- 并行是指同时执行。
- 并发是指多线程执行,微观串行,宏观并行。(交替执行)
悲观锁乐观锁:
- 悲观锁是指普通的互斥锁,每次使用无论读写都加锁,性能较低
- 乐观锁是指在读取时无锁,在写入时,需要先比较版本,相同时才能修改,否则取消
二十三、 Queue
队列,遵循先进先出的原则,即FIFO(First In First Out)
常用方法:
- add() 添加元素,推荐使用offer()
- remove() 获得元素并删除,推荐使用poll()
- element()获得元素不删除,推荐使用keep()
23.1 ConcurrentLinkedQueue
线程安全的Queue,使用CAS算法,高并发下性能最好的queue。
23.2 阻塞队列BlockingQueue接口
添加了两个无限期等待(wait)的方法:
- put() 添加元素
- take() 取出并移出元素
用来解决生产消费问题。
23.2.1 实现类ArrayBlockingQueue
使用数组结构实现的有界队列,需要在创建时指定大小。
ArrayBlockingQueue queue = new ArrayBlockingQueue(10);
23.2.2 实现类LinkedBlockingQueue
使用链表结构实现的无界队列,上限为Integer.MAX_VALUE。
LinkedBlockingQueue queue = new LinkedBlockingQueue();
23.2.3 实现生产消费模式
public class Test {
public static void main(String[] args) {
// 创建一个只能存储6个元素的仓库
ArrayBlockingQueue<Car> queue = new ArrayBlockingQueue<Car>(6);
// 创建生产者
Runnable p = new Runnable() {
@Override
public void run() {
String name = Thread.currentThread().getName();
for (int i = 0; i < 15; i++) {
Car car = new Car(name + "===" + i);
try {
queue.put(car);
System.out.println("生产了一辆车," + car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 创建消费者
Runnable c = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Car car = queue.take();
System.out.println("消费了一辆车," + car);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(p);
pool.submit(p);
pool.submit(c);
pool.submit(c);
pool.submit(c);
pool.shutdown();
}
}