一、线程基础
1.1定义和优点
进程:应用程序中的执行实例,有独立的内存空间和系统资源。操作系统中的最小单位,通过进程可以创建程序的应用。
线程:cpu调度和分派的基本单位,进程中执行运算的最小单位。
多线程:如果一个进程中同时运行多个线程,用来完成不同的工作,则称为“多线程”,“多线程”是多个线程交替占用CPU资源,并非真正的并行执行。
“多线程”的好处:
- 充分利用CPU资源
- 简化编程模型
- 带来良好的用户体验
1.2创建方式
创建线程的方式:
- 继承Thread类——编写简单,可直接操作线程,用于单继承
- 实现Runnable接口——避免了单线程的局限性(推荐使用)
所有线程要执行的内容都放置在run方法中,但是启动线程要利用start方法。
二、线程状态
2.1线程状态概述
创建状态:线程刚被创建,但并未启动,还没有调用start方法。只有线程对象,没有线程特征。
就绪状态:线程可以在Java虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决于操作系统处理器。调用了start方法。
运行状态:就绪状态的线程获取到了CPU的资源,执行了代码。线程获取 CPU权限进行执行,线程只能从就绪状态到运行状态。
阻塞状态:阻塞状态是线程因为某种原因放弃cpu使用权,暂时停止运行,直到阻塞条件被终止或结束才会正常运行。
阻塞状态分为三种:
锁阻塞:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
无限等待:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
计时等待:同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。
死亡状态:因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。
2.2线程调度
getPriority():返回线程优先级。
setPriority(int):更改线程优先级(默认为5,最小为1,最大为10)。
sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠。
join():等待该线程终止,其他线程全部等待,具有强制性。
yield():暂停当前正在执行的线程,并执行其他线程。
Interrupt():中断线程。
isAlive():测试线程是否处于活跃状态。
2.3线程通信
wait():让当前线程进入到等待状态 此方法必须锁对象调用。
notify():唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用。
三、volatile关键字
3.1定义
volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值。
3.2使用场景
当两个线程分别对一个变量进行读写操作,可能出现一个线程对变量的值做出修改但另一个线程无法感知的情况。
3.3 volatile与synchronized
1、volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
2、volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。
四、原子性
4.1定义
概述:所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
4.2遇到问题
这个问题主要是发生在count++操作上:
count++操作包含3个步骤:
1、从主内存中读取数据到工作内存
2、对工作内存中的数据进行++操作
3、将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
示例:
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的遍历
private int count = 0 ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
count++ ;
System.out.println("count =========>>>> " + count);
}
}
}
public class VolatileAtomicThreadDemo {
public static void main(String[] args) {
// 创建VolatileAtomicThread对象
VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;
// 开启100个线程对count进行++操作
for(int x = 0 ; x < 100 ; x++) {
new Thread(volatileAtomicThread).start();
}
}
}
执行结果:不保证一定是10000
4.3volatile原子性测试
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全),所以在多线程环境下,要保证数据的安全性,还需要使用锁机制。
4.4问题解决
java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
示例:
public class VolatileAtomicThread implements Runnable {
// 定义一个int类型的变量
private AtomicInteger atomicInteger = new AtomicInteger() ;
@Override
public void run() {
// 对该变量进行++操作,100次
for(int x = 0 ; x < 100 ; x++) {
int i = atomicInteger.getAndIncrement();
System.out.println("count =========>>>> " + i);
}
}
}
五、并发包
5.1ConcurrentHashMap
1、为什么要使用ConcurrentHashMap:
1.HashMap线程不安全,会导致数据错乱
2.使用线程安全的Hashtable效率低下基于以上两个原因,便有了ConcurrentHashMap的登场机会。
2、HashTable:
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。
5.2CountDownLatch
1、CountDownLatch允许一个或多个线程等待其他线程完成操作,再执行自己。
2、例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行。
3、CountDownLatch中的方法:
downLatch.countDown(); //数量-1
downLatch.await();//等待计数器归零,然后再往下执行
每次线程调用countDown()数量-1,假设计数器变成0,downLatch.await()就会被唤醒,继续执行!
4、说明:
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。
CountDownLatch是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器-1,当计数器到达0时,调用CountDownLatch。
await()方法的线程阻塞状态解除,继续执行。
5.3CyclicBarrier
1、CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
2、例如:公司召集5名员工开会,等5名员工都到了,会议开始。
我们创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。
3、CyclicBarrier的重要方法:
await():每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier cbRef = new CyclicBarrier(5, new MeetingThread());
等待5个线程 执行完毕,再执行MeetingThread
4、使用场景
使用场景:CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。
5.4Semaphore
1、定义
Semaphore(发信号)的主要作用是控制线程的并发数量。
synchronized可以起到"锁"的作用,但某个时间段内,只能有一个线程允许执行。
Semaphore可以设置同时允许几个线程执行。
Semaphore字面意思是信号量的意思,它的作用是控制访问特定资源的线程数目。
2、Semaphore的方法
acquire():表示获取许可。
release():表示释放许可。
private Semaphore semaphore = new Semaphore(1);
1表示许可的意思,表示最多允许1个线程执行acquire()和release()之间的内容
5.5Exchanger
1、定义
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。
这两个线程通过exchange方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
2、Exchanger的方法
exchange(x)为要交换的值。
六、线程池
6.1线程池好处
1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
6.2线程池的使用
1、创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);
2、创建Runnable接口子类对象
MyRunnable r = new MyRunnable();
3、提交Runnable接口子类对象
service.submit(r);
4、关闭线程池(一般不做)
service.shutdown();
七、死锁
7.1什么是死锁
在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。
7.2产生死锁的条件
1.有多把锁
2.有多个线程
3.有同步代码块嵌套
注意:我们应该尽量避免死锁