多线程
并发(Concurrent):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行(Parallel):当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。
进程(Process):进程是正在运行的程序实体,并且包含这个运行的程序中占占据的所有系统资源。
**线程 **(Thread):线程是一个程序内部的一条执行路径,它是操作系统能够进行运算调度的最小单位,它被包含在进程中实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以进行多个线程,每条线程并行执行不同的任务。
如果一个进程中同时运行了多个线程(多个线程交替占用CPU资源,而非真正的并行执行),用来完成不同的工作,则称之为多线程
1、多线程优点
- 充分利用CPU的资源
- 简化编程模型
- 带来良好的用户体验
2、缺点
- 对线程进行管理要求额外的 CPU开销,线程的使用会给系统带来上下文切换的额外负担。
3、多线程的创建方式
3.1继承Thread类
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于扩展
package com.csi;
public class ThreadTest extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "在执行");
}
}
public static void main(String[] args) {
Thread thread1 = new ThreadTest();
Thread thread2 = new ThreadTest();
thread1.start();
thread2.start();
}
}
/**
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
*/
运行结果每次不一定相同
3.2实现Runnable接口
优点:线程任务类只是实现任务接口,可以继续继承其他类和实现其他的接口,扩展性强
缺点:编程多一层对象的包装,如果线程有执行结果不能直接返回
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "在执行");
}
}
}
public class TestRunnable {
public static void main(String[] args) {
Runnable myRunnable1 = new MyRunnable();
Runnable myRunnable2 = new MyRunnable();
Thread thread1 = new Thread(myRunnable1);
Thread thread2 = new Thread(myRunnable2);
thread1.start();
thread2.start();
}
}
/*
Thread-1在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-1在执行
Thread-0在执行
Thread-0在执行
*/
3.3实现Callable接口
优点:如果线程有执行结果可以直接返回,线程任务类只是实现任务接口,可以继续继承其他类和实现其他的接口,扩展性强
缺点:编码复杂
public class CallableTest implements Callable<Integer> {
private int count;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 100; i++) {
count += i;
}
return count;
}
}
package com.csi;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestCallable {
public static void main(String[] args) {
Callable call = new CallableTest();
FutureTask futureTask = new FutureTask(call);
Thread thread = new Thread(futureTask);
thread.start();
try {
Object count = futureTask.get();
System.out.println(count);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}
//5050
3.4实现Runnable接口和继承Thread的区别
继承Thread一个类只能继承一个父类,存在局限;一个类可以实现多个接口,扩展性增强
4.线程的调度
- setPriority(int newPriority):可以调节1~10,线程优先级默认为5
- sleep(long millis):线程进入休眠状态,并且不释放锁(若有锁),休眠结束后直接进入就绪状态
- yield():使CPU暂时放弃对该线程的调度,执行其他的线程,但是不一定暂停。
- interrupt():终端当前线程
- isAlive():测试当前线程是否存活,返回boolean值
5、线程的同步
当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。
5.1同步方法
即有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
5.2同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步
6、等待和唤醒
public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用
public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
案例:包子铺制作包子后唤醒消费者线程,随后进入休眠状态,消费者线程买完包子,唤醒包子铺线程制作包子,瑞后进入休眠状态
//包子铺类
public class BunStore implements Runnable{
private List list;
public BunStore(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
if (list.size() > 0){
try {
System.out.println("等待消费者消费包子");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(0,"包子");
System.out.println("生产了一个包子");
list.notify();
}
}
}
}
//消费者类
public class Custormer implements Runnable{
private List list;
public Custormer(List list) {
this.list = list;
}
@Override
public void run() {
while (true) {
synchronized (list) {
if (list.size() == 0){
try {
System.out.println("等待生产者生产包子");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费包子
list.remove(0);
System.out.println("消费了一个包子");
list.notify();
}
}
}
}
//测试类
public class Test {
public static void main(String[] args) {
List list = new ArrayList();
BunStore bunStore = new BunStore(list);
Custormer custormer = new Custormer(list);
Thread thread1 = new Thread(bunStore);
Thread thread2 = new Thread(custormer);
thread1.start();
thread2.start();
}
}
7、volatile关键字
volatile也是java提供的一种轻量级的同步机制,但是volatile只能保证可见性,无法保证其原子性
public class VolatileThread extends Thread{
private volatile boolean flag = false;
private boolean isFlag(){
return flag;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;
System.out.println("flag=" + flag);
}
public static void main(String[] args) {
VolatileThread volatileThread = new VolatileThread();
volatileThread.start();
while (true){
if (volatileThread.isFlag()){
System.out.println("执行了");
}
}
}
}
若没有volatile 修饰则主线程中无法获取flag修改过后的值,则以一直在死循环中
使用场景:
利用可见性特点,控制某一段代码执行或者关闭
多个线程操作共享变量,但是是有一个线程对其进行写操作,其他的线程都是读
8、JMM
概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将
变量存储到内存和从内存中读取变量这样的底层细节。
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变
量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
9、AtomicInteger关键字
在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)
public class VolatileAtomicThread implements Runnable{
private volatile int count = 0 ;
private static final Object obj = new Object();
@Override
public void run() {
for(int x = 0 ; x < 100 ; x++) {
synchronized (obj) {
count++ ;
System.out.println("count= " + count);
}
}
}
public static void main(String[] args) {
VolatileAtomicThread vat = new VolatileAtomicThread();
Thread thread1 = new Thread(vat);
Thread thread2 = new Thread(vat);
thread1.start();
thread2.start();
}
}
10、原子类CAS机制实现线程安全。
CAS的全称是: Compare And Swap(比较再交换); 是现代CPU广泛支持的一种对内存中的共享数据进行操作
的一种特殊指令。CAS可以将read-modify-check-write
转换为原子操作,这个原子操作直接由处理器保证。
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
举例:
- 在内存地址V当中,存储着值为10的变量。
- 此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
-
在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
-
线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
- 线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
- 这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
- 线程1进行SWAP,把地址V的值替换为B,也就是12。
11、ConcurrentHashMap
为什么要使用ConcurrentHashMap?
-
HashMap线程不安全,会导致数据错乱
-
使用线程安全的Hashtable效率低下
public class Const extends Thread{ public static HashMap<String,String> map = new HashMap<>(); public void run(){ for (int i = 0; i < 500000; i++) { Const.map.put(this.getName() + (i + 1), this.getName() + i + 1); } } public static void main(String[] args) { Const a1 = new Const(); Const a2 = new Const(); a1.setName("线程1-"); a2.setName("线程2-"); a1.start(); a2.start(); //休息10秒,确保两个线程执行完毕 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { throw new RuntimeException(e); } //打印集合大小 System.out.println("Map大小:" + Const.map.size()); } } //Map大小:943870
两个线程分别向同一个map中写入50000个键值对,最后map的size应为:100000
为了保证线程安全,可以使用ConcurrentHashMap
package com.csi; import java.util.concurrent.ConcurrentHashMap; public class Const extends Thread{ public static ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>(); public void run(){ for (int i = 0; i < 500000; i++) { Const.map.put(this.getName() + (i + 1), this.getName() + i + 1); } } public static void main(String[] args) { Const a1 = new Const(); Const a2 = new Const(); a1.setName("线程1-"); a2.setName("线程2-"); a1.start(); a2.start(); //休息10秒,确保两个线程执行完毕 try { Thread.sleep(1000 * 5); } catch (InterruptedException e) { throw new RuntimeException(e); } //打印集合大小 System.out.println("Map大小:" + Const.map.size()); } } /* Map大小:1000000 */
ConcurrentHashMap能保证结果正确,而且提高了效率。
12、CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。
例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印
C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。
ThreadAC类
package com.csi;
import java.util.concurrent.CountDownLatch;
public class ThreadAC extends Thread{
private CountDownLatch down ;
public ThreadAC(CountDownLatch down) {
this.down = down;
}
@Override
public void run() {
System.out.println("A");
try {
down.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("C");
}
}
ThreadB类
package com.csi;
import java.util.concurrent.CountDownLatch;
public class ThreadB extends Thread{
private CountDownLatch down ;
public ThreadB(CountDownLatch down) {
this.down = down;
}
@Override
public void run() {
System.out.println("B");
down.countDown();
}
}
测试类
package com.csi;
import java.util.concurrent.CountDownLatch;
public class Demo {
public static void main(String[] args) {
CountDownLatch down = new CountDownLatch(1);//创建1个计数器
new ThreadAC(down).start();
new ThreadB(down).start();
}
}
//结果
/*
A
B
C
*/
会保证按:A B C的顺序打印。
CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()
方法让计数器-1,当计数器到达0时,调用CountDownLatch。
await()方法的线程阻塞状态解除,继续执行。
13、CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一
个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线
程才会继续运行。
使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
14、Semaphore
Semaphore(发信号)的主要作用是控制线程的并发数量。
synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore可以设置同时允许几个线程执行。
Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
public Semaphore(int permits) permits 表示许可线程的数量
public Semaphore(int permits, boolean fair) fair 表示公平性,如果这个设为 true的话,下次执行的线程会是等待最久的线程
示例:同时允许2个线程同时执行
Service类:
package com.csi;
import java.util.concurrent.Semaphore;
public class Service {
private Semaphore semaphore = new Semaphore(2);
public void testMethod() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()
+ " 进入 时间=" + System.currentTimeMillis());
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName()
+ " 结束 时间=" + System.currentTimeMillis());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
线程类:
package com.csi;
public class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
super();
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
测试类
package com.csi;
public class DemoSemaphore {
public static void main(String[] args) {
Service service = new Service();
//启动5个线程
for (int i = 1; i <= 5; i++) {
ThreadA a = new ThreadA(service);
a.setName("线程 " + i);
a.start();//5个线程会同时执行Service的testMethod方法,而某个时间段只能有1个线程执行
}
}
}
//结果
/*
线程 3 进入 时间=1689601225566
线程 1 进入 时间=1689601225566
线程 3 结束 时间=1689601230590
线程 1 结束 时间=1689601230590
线程 5 进入 时间=1689601230590
线程 4 进入 时间=1689601230590
线程 5 结束 时间=1689601235591
线程 2 进入 时间=1689601235591
线程 4 结束 时间=1689601235605
线程 2 结束 时间=1689601240593
*/
15、Exchanger
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程
也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据
传递给对方。
使用场景
使用场景:可以做数据校对工作
需求:比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用AB岗两人进
行录入,录入到两个文件中,系统需要加载这两个文件,
并对两个文件数据进行校对,看看是否录入一致
16、线程池
线程池其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
16.1优点:
-
降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
-
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
16.2线程池的使用
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService 。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。
16.3创建线程池
使用线程池中线程对象的步骤:
-
创建线程池对象。
-
创建Runnable接口子类对象。(task)
-
提交Runnable接口子类对象。(take task)
-
关闭线程池(一般不做)。
17、死锁
概念:在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了,尽量避免死锁
17.1产生死锁的条件
- 有多把锁
- 有多个线程
- 有同步代码块嵌套
17.2死锁代码
public class Demo05 {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
new Thread(mr).start();
new Thread(mr).start();
}
}
class MyRunnable implements Runnable {
Object objA = new Object();
Object objB = new Object();
/*
嵌套1 objA
嵌套1 objB
嵌套2 objB
嵌套1 objA
*/
@Override
public void run() {
synchronized (objA) {
System.out.println("嵌套1 objA");
synchronized (objB) {// t2, objA, 拿不到B锁,等待
System.out.println("嵌套1 objB");
}
}
synchronized (objB) {
System.out.println("嵌套2 objB");
synchronized (objA) {// t1 , objB, 拿不到A锁,等待
System.out.println("嵌套2 objA");
}
}
}
}