- 1. 线程
- 1.1 线程出现的目的
- 1.2 线程的优点有哪些?
- 1.2 操作系统的线程和Java中的线程
- 2. JAVA中创建线程
- 2.1 继承Thread类,重写run()方法
- 2.2 实现Runnable接口
- 2.3 利用Callable和FutureTask接口实现
- 2.4 方法之间的比较
- 2.5 线程的操作
- 2.6 线程的状态
- 3. 线程安全
- 3.1 线程不安全的原因
- 3.2 保持线程安全的方法
- 4. 线程同步的方法
- 4.1 synchronized 关键字-监视器锁monitor lock
- 4.2 volatile 关键字
- 4.3 Java 标准库中的线程安全类
- 5. 线程通信 Wait()和Notify() (线程之间相互通信)
- 6. 线程池
- 6.1 定义与使用原因
- 6.2 线程池的创建
- 6.3 **线程池面试常考**
- 7. 锁策略
- 7.1 常见的锁策略
- 7.2 CAS
- 7.2.1 什么是CAS
- 7.2.2 CAS 实现了原子类
- 7.2.3 CAS 实现了自旋锁
- 7.2.4 CAS 解决ABA问题(即同时进行为修改与读写)
- 9. synchronized:美[ˈsɪŋkrəˌnaɪz]
- 9.1 特性
- 10. Lock
- 10.1 定义
- 10.2 Lock比synchronized多的功能
- 10.3 Lock与synchronized线程通信的区别
- 10.4 Lock与synchronized在锁类型的区别
- 引入进程的目的是为了并发编程,但是进程能够解决一定程度的并发问题,还不够,有两个问题
(1):创建进程需要分配资源
(2):销毁进程需要释放资源
(3):频繁进程和线程的切换会导致开销较大,故设计了一个轻量级的进程,即线程(Thread)
(1):创建,销毁和调度的速度都比进程快。
(2):线程就是程序内部的一条执行路径。单个路径就是单线程。
- 操作系统的线程:通过使用PCB来进行描述
- Java中的线程:通过使用Thread类进行描述
- 线程实现的四步曲
1) 定义一个线程类MyThread,继承Thread类,重写run方法
2)新建线程对象
3)调用start()方法启动 - 方法特点
1)优点:代码简单
2)缺点:线程类已经继承了Thread,不能够再继承其他类,不利于扩展,线程有执行结果,不可以返回。 - 代码展示
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
//新建一个线程的实例,并调用start进行执行
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
t.start();
}
}
- 问题:为什么不直接调用run方法,而是调用start启动线程
1) 直接调用run方法,会被当成普通方法进行执行,此时相当于还是单线程执行
2)使用 t.run() 会直接调用 myThread.run()
3)只有调用start方法,才是启动一个新的线程执行
4)使用 t.start() 会创建一个新的 PCB ,新的 PCB 链接在链表上,然后执行 myThread.run() 方法
- 线程实现的四步曲
1) 定义一个线程类MyRunnable,实现Runnable接口,重写run方法
2)新建线程对象MyRunnable
3)把MyRunnable对象交给Thread对象处理
4)启动线程t.start() - 特点
1)优点:只是实现接口,还可以继承其他类,可扩展性强
2)缺点:编程多一层对象包装,线程有执行结果,不可以返回。即run方法无返回值。 - 代码展示
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
- 使用匿名内部类,直接重写Runnable的run方法,然后新建Thread类对象,并启动start()
public class demo1 {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
System.out.println("i="+i);
}
}
};
Thread thread=new Thread(runnable);
thread.start();
}
}
2.3 利用Callable和FutureTask接口实现
- 前两个方法的缺点
1) 重写的run()方法均不能直接返回结果
2)不适合需要返回线程执行结果的业务场景 - 解决办法
1) JDK5.0 提供了Callable和Future Task来实现
2) 可以得到线程执行的结果 - 特点
1) 优点:实现接口,还可以实现其他的接口和父类,可拓展性强;同时可以得到线程执行的结果
2) 缺点:代码复杂了一点 - 实现步骤
1) 定义一个任务类MyCallable,实现Callable接口,重写call方法(封装要做的事情,可以带返回值)
2)新建MyCallable实例对象;新建Future Task实例对象,然后把MyCallable实例对象放进去(用Future Task把Callable对象封装成线程任务对象)。
3)新建Thread类对象,把MyCallable实例对象交给Thread处理。
4)调用Thread的start()进行线程的启动,执行任务
5)线程执行完毕后,调用Future Task实例对象的get方法,获取任务执行的结果。 - 代码展示
class MyCallable implements Callable{ private int n; public MyCallable(int n) { this.n = n; } @Override public String call() throws Exception { int sum = 0; for (int i = 1; i <= n ; i++) { sum += i; } return "子线程执行的结果是:" + sum; } }
public class ThreadDemo3 {
public static void main(String[] args) {
// 3、创建Callable任务对象
Callable call = new MyCallable(100);
// 4、把Callable任务对象 交给 FutureTask 对象
// FutureTask对象的作用1: 是Runnable的对象(实现了Runnable接口),可以交给Thread了
// FutureTask对象的作用2: 可以在线程执行完毕之后通过调用其get方法得到线程执行完成的结果
FutureTask f1 = new FutureTask<>(call);
// 5、交给线程处理
Thread t1 = new Thread(f1);
// 6、启动线程
t1.start();
Callable call2 = new MyCallable(200);
FutureTask f2 = new FutureTask<>(call2);
Thread t2 = new Thread(f2);
t2.start();
try {
// 如果f1任务没有执行完毕,这里的代码会等待,直到线程1跑完才提取结果。
String rs1 = f1.get();
System.out.println("第一个结果:" + rs1);
} catch (Exception e) {
e.printStackTrace();
}
try {
// 如果f2任务没有执行完毕,这里的代码会等待,直到线程2跑完才提取结果。
String rs2 = f2.get();
System.out.println("第二个结果:" + rs2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.4 方法之间的比较
2.5 线程的操作内容参考
黑马程序员:https://www.bilibili.com/video/BV1Cv411372m?spm_id_from=333.337.search-card.all.click
- 线程休眠: t.sleep(500),精确时间休眠,结束还是需要进行资源抢夺
- 线程中断:t.join(),进入阻塞等待
- Thread常用方法和构造器
| 方法名称 | 说明 |
|---|---|
| String getName() | 获取当前线程的名称 |
| void setname(String name) | 设置线程名字 |
| public static ThreadcurrentThread() | 返回对当前正在执行线程对象的引用 |
| public static void sleep(long time)() | 让线程休眠指定的时间,单位为毫秒 |
| public void run() | 线程任务方法 |
| public void start() | 线程启动方法 |
线程共有六种状态
- New(新建):线程刚被新建出来,内核没有创建PCB— ** -创建线程对象**
- Runnable(可运行):调用了start()方法,处于可运行状态,等待获取CPU使用权---- ** start()方法**
- Blocked:阻塞状态,因为某种原因放弃CPU使用使用权,处于阻塞队列中 -------- ** 无法获得所对象**
1) 等待阻塞:使用了wait()
2)同步阻塞:运行线程在获取对象同步锁的时候,发现已被抢占,故等待
3)其他阻塞:执行sleep和join方法,或者是发出I/O六请求 - Waiting(无限等待):一个线程进入waiting状态,只能motify和notifyAll才能够唤醒 ------- ** wait()**
- Time Waiting(有限等待):同waiting转态,只是多了计时 -------- ** sleep()**
- Teminated:死亡状态,线程执行完成或者异常退出 ------ ** 全部代码运行结束**
- 线程之间是并发执行的抢占式的:没有顺序,但是有些功能需要线程间有顺序(根本原因)
- 多个线程可能对同一个变量进行修改;一个线程在改,另一个线程在读,会发生错乱。
- 多个线程可能对同一个变量进行访问:
- 可见性:线程之间的可见性,一个线程修改的状态对另一个线程是可见的。(volatile、synchronized 和 final )
- 原子性:原子具有不可分割性,一个操作如果具有原子性,那么它是不可分割的,连续的。( synchronized 和在 lock、unlock )
- 有序性:线程之间的操作时有序的(volatile、synchronized)
- 方法一:同步代码块,基于synchronized,范围小,加锁局部代码,synchronized(Object o ){}
- 方法二:同步方法,基于synchronized,范围大(默认用this或者当前类class对象作为锁)
- 方法三:Lock锁:比synchronized更加广泛的锁定操作。接下来会讲
- 修饰的对象与锁住的对象
1) 修饰同步代码块:作用于调用的实例对象(比普通方法更加灵活,效率更高)
2) 修饰普通方法:作用于调用的实例对象
3)修饰静态方法/修饰类:作用于所有对象
参考文章:我们锁住的到底是什么 - synchromized的特性
1)互斥性:进行 synchromized修饰的对象中即为加锁;退出修饰对象中即为解锁(自动进行)
2)阻塞等待:针对每一把锁维护一个等待序列,锁被占用,则线程进行等待队列中等待(上一个进程解锁后,需要换新需要锁的进程,并且需要竞争)
3) 刷新内存:每次操作都进行Load和Save操作,刷新内存中的值。(可能会导致程序运行变慢)
4)可重入:允许一个线程针对同一把锁,进行连续加锁。你会发生死锁现象。(synchronized和ReentrantLock都是可重入锁,而lock是不可重入锁) - 保证线程安全的特性原理:
1)可见性:保持操作是在主内存进行的。实时进行更新,不设置线程缓存
2)原子性:加锁,不能被其他线程访问。
3)有序性:同一个时刻只允许一条线程对其进行读写操作
-
定义:volatile变量是一种比sychronized关键字更轻量级的同步机制。
-
volatile是如何保证线程同步的?
1)可见性::一个线程修改了volatile修饰的变量,会立即同步到主内存中,每次调用都通过主内存完成。不允许线程内部
2)有序性:内存屏障和禁止重排序 -
特性
1)volatile修饰的变量读操作不增加消耗,但是写操作会增加内存消耗
2)volatile不是安全的:volatile保证了写操作能够反映到每一个线程中,但是里面的运算不是原子操作,不是安全的。 -
适用的场合
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量不变的式子中
因此,volatile适用于状态标记量:
- 线程不安全:ArrayList,LinkedList;HashSet,TreeSet;HashMap,TreeMap;StringBuilder
- 线程安全:ConcurrentHashMap,StringBuffer(核心方法都带有synchronized),String(虽然没加锁,但是是不可变独享,因为被final修饰)
- 功能:用于线程之间的同步。
- 定义:Wait()和Notify()必须在synchronized保护的代码中使用,说明一个线程暂时进入阻塞队列中,和其他唤醒其他线程获得对象锁。
- 种类
1) wait():导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法
2)notify():唤醒在此对象监视器上等待的单个线程
- notifyAll():唤醒在此对象监视器上等待的所有线程
- 特点:
1)wait( ),notify( ),notifyAll( )都不属于Thread类,而是属于Object基础类。
2)每个对象都可以作为锁,故需要定义操作的方法也应该是对象,而不是线程。
//https://blog.csdn.net/jushisi/article/details/103240664 - ** wait 和 sleep 的 区别(面试题)**
1)wait()能够使得某个线程进入阻塞状态,有限或者无限时间;sleep()使线程等待有限时间
2)wait 需要搭配synchronized 使用,sleep不需要
3)sleep()唤醒两个方法(时间到和调用该线程的interrupted 方法);wait()方法会多一个notify()唤醒
4)wait()是为了配合多线程的执行顺序,而sleep没有多线程的关系
5)wait()是Object(()方法,而sleep是Thread类的静态方法
- 定义:线程池是一个复用线程的技术
- 原因:如果每一个用户请求,都创建一个新线程来完成,那么创建新线程开销很大,会严重影响系统的性能。
- 使用ExecutorService的实现类ThreadPoolExecutor进行创建线程池对象(七大参数)
1) corePoolSize:指定线程池的线程数量(核心线程)
2)maxmumPlloSize:指定线程池可支持的最大线程数(核心线程+临时线程)
3)keepAliveTime:临时线程的最大存活时间
4)unit:指定临时线程存活时间的单位(秒,分,时,天)
5)workQueue:指定任务队列,新任务来了的缓冲区
6)threadFactory:指定用哪个线程工厂创建线程
7)handler:指定线程忙,任务满的时候,新任务来了怎么办
- 临时线程在什么时候创建啊?
新任务提交时,核心线程都在忙,任务队列也满了,这个时候创建临时线程。 - 什么时候开始拒绝任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候,才会开始拒绝任务
- 乐观锁和悲观锁
1)乐观锁:总是假设最好的情况,认为共享资源没有修改;当对数据进行提交更新的时候,会判别一下数据有没有被修改,被修改了则不提交(使用版本号控制如果资源被修改过,那么版本号会加1,然后更新主内存资源;后面还想更新,版本还是之前的,就更新不了了)
2)悲观锁:总是假设最坏的情况,每次去修改数据,都认为别人也会去修改,所以在打算修改数据的时候,直接加锁(其他的不能进行读写操作),当修改完后,进行解锁。 - 读写锁
1)线程主要对数据就是读操作和写操作,读操作不需要线程互斥,只有设计写操作需要线程互斥,这个时候定义一个读写锁,非常适合读多写少的情况,能够大大降低性能。 - 重量级锁和轻量级锁
1)一个开销小,由用户态完成
2)一个开销大,由核心态完成 - 自旋锁(Spin Lock) 挂起等待锁
1)自旋锁:在线程抢锁失败后,不是处于挂堵塞状态,而是快速进入下一次抢锁状态,栈CPU,但是能够第一时间获取锁。
2)挂起等待锁:抢锁失败后,处于阻塞等待状态,不占CPU - 公平锁和不公平锁
1)公平锁:遵循先来后到的原则
2)不公平锁:不支持先来后到,抢锁状态人人平等。 - 可重入锁 和不可重入锁
1)可重入锁:允许同一个线程多次获取同一把锁(lock和synchronize都是可重入锁)
2)不可重入锁:不允许线程多次获取同一把锁。
- CAS为compare and switch,比较与交换,可以认为是一种乐观锁。
- 这个操作原子性的,CPU提供了一组CAS相关的指令
(1):比较A和B是否相等
(2):如果相等,将C写入A中(进行交换)
(3):返回操作时true(交换成功)或者false(交换失败)
boolean CAS(value,oldvalue,expectvalue){
if(value == oldvalue){
value = expectvalue;
return true;
}
return false;
}
7.2.2 CAS 实现了原子类
CAS本来就是具有原子性的,故能够很方便的实现i++操作
boolean CAS(value,oldvalue,oldvalue+1){
if(value == oldvalue){
value = expectvalue;
return true;
}
return false;
}
7.2.3 CAS 实现了自旋锁
public class SpinLock {
private Thread owner = null;
public void lock() {
while(!CAS(this.owner, null , Thread.currentThread())){
}
}
public void unlock(){
this.owner = null;
}
}
(!CAS(this.owner, null , Thread.currentThread()))
(1):通过一个CAS来判断当前锁是不是被某个线程持有
(2):如果被某个线程持有(this.owner!=null),那么就让锁进行自旋状态
(3):如果没有被某个线程持有(this.owner==null),那么就让申请的线程获得锁
引入版本号,:当前的版本号和读取的版本号相同,则进行数据的修改,并且是版本号加一;否则放弃修改。
9. synchronized:美[ˈsɪŋkrəˌnaɪz] 9.1 特性- 开始是 乐观锁 ,如果 锁冲突频繁 ,那么就变为了 悲观锁
- 开始是 轻量级锁 ,如果所 持有的的时间比较长 ,那么就转变为 重量级锁
- 实现轻量级锁的时候,大概率用到了自旋锁
- 是一种不公平锁,不支持先来后到
- 是一种可重入锁,一个线程能够多次获取同一锁对象
- 不是一种读写锁
- Lock是一种比synchronized更加灵活的锁,具有更广泛的使用性。
- 尝试非阻塞地获取锁:尝试去获取锁,如果没获取成功,也不会进入阻塞状态。
- 能被中断地获取锁:获取到的锁能够响应中断,中断发生后,锁会被释放
- 超时获取锁:在一个指定的时间之前要获得锁,如果那么就会返回,不再请求锁。
1)synchronized中,使用wait和notify,notifyall来进行线程间的通信,但是notify,具体是哪一个线程来获取锁,这是由JVM决定的
2)Lock使用Condition接口与newCondition() 方法实现线程通信,使用Condition接口可以选择性通知某一个线程,更加准确和灵活。
1)synchronized是不公平锁,固定了,改变不了
2)Lock可以是公平锁, 也可以是不公平锁,可以自行设定。
文章参考:
volatile 关键字参考文章
https://blog.csdn.net/wwzzzzzzzzzzzzz/article/details/123680001



