文章目录
一,创建线程的三种方式
1.继承Thread类
- 创建MyThread类,继承java.lang.Thread
- 重写run方法
- 创建测试类和对象,创建MyThread对象,并执行start方法
//1.继承Thread类 2.重写run()方法 3.调用start()方法开启线程
public class MyThread extends Thread{
@Override
public void run() {
//run 方法流程体
for (int i = 0; i < 10; i++) {
System.out.println("====================");
}
}
public static void main(String[] args) {//main 线程,主线程
//start 方法开启线程
MyThread MyThread = new MyThread();
MyThread.start();
for (int i = 0; i < 5; i++) {
System.out.println("----------------------------------");
}
}
}
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-28ZrQFXy-1691803607960)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230811194051442.png)]
2.实现runnable接口
- 创建一个类Run,实现java.lang.Runnable接口
- 重写run方法
- 在测试类中创建Run的对象run
- 创建Thread对象并将run传入
- 调用start方法
注:一般使用匿名内部类的方式实现创建
//使用Runnable实现自助卖票功能
public static void main(String[] args) {
Runnable runnable = new Runnable() {
private int tickets = 100;
@Override
public void run() {
while (true){
//有票
if(tickets > 0){
System.out.println(Thread.currentThread().getName()+",售出票号: "+tickets);
tickets--;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
//票已售罄
System.out.println(Thread.currentThread().getName()+",票已售罄");
break;
}
}
}
};
Thread t1 = new Thread(runnable,"窗口1");
t1.start();
Thread t2 = new Thread(runnable,"窗口2");
t2.start();
Thread t3 = new Thread(runnable,"窗口3");
t3.start();
}
结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1cBkXaa6-1691803607962)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20230811194715027.png)]
3.实现callable接口
- 创建类Call,实现java.lang.Callable接口
- 重写call方法,需要返回值。
- 实现Call,并将此对象作为参数传入FutureTask的构造函数中,创建FutureTask对象
- 创建Thread对象,并将FutureTask对象传入
- 调用Thread.start();
注:Callable可以理解为是带有返回值的Runnable,是对Runnable的补充,可以通过FutureTask对象的get方法获取call方法的返回值。
4.使用Runnable的优势
-
可以避免java代码单继承的局限性
-
设置线程和开启线程进行分离(解耦)
• 在实现类中重写run方法,增加线程任务逻辑
• 创建Thread类对象,调用start方法,用来开启线程
二,线程同步
当两个或多个线程需要访问同一资源时,需要确保该资源某一时刻只能被一个线程使用。
解决办法:
- 同步代码块
- 同步方法
- 锁机制
1.同步代码块
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其它线程只能在外等。
格式:
//同步锁
Object lock = new Object;
synchronized(lock){
//需要同步操作的代码
}
接下来尝试使用同步代码块的方法对上面的售票类进行线程同步:
public class SellTrainTicketSyncCode {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
private int tickets = 100;
Object lock = new Object();
@Override
public void run() {
while (true) {
synchronized (lock) {
//有票
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ",售出一张票: " + tickets);
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//票已售罄
System.out.println(Thread.currentThread().getName() + ",票已售罄");
break;
}
}
}
}
};
Thread t1 = new Thread(runnable, "窗口1");
t1.start();
Thread t2 = new Thread(runnable, "窗口2");
t2.start();
Thread t3 = new Thread(runnable, "窗口3");
t3.start();
}
}
**注意:**应该将同步锁写在循环内部,如下:
...... while (true) { synchronized (lock) { ...... } } ......
这样每次循环都将尝试去获得锁,而应该将锁写在循环外面,如下:
...... synchronized (lock) { while (true) { ...... } } ......
这样直到循环执行完毕才会退出锁
2.方法同步
保证一个线程正在调用该方法时,其他线程只能在方法外面等。
格式:
public synchronized void method(){
//可能会产生线程安全问题的代码
//(即上述循环内的代码)
}
使用方法同步改造售票类:
public class D4SellTrainTicketSyncMethod2 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
private int tickets = 100;
@Override
public void run() {
while (tickets >=0) {
sellTicket();
if (tickets == 0) break;
}
}
//同步方法
private synchronized void sellTicket() {
//有票
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + ",售出一张票: " + tickets);
tickets--;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
//票已售罄
System.out.println(Thread.currentThread().getName() + ",票已售罄");
}
}
};
Thread t1 = new Thread(runnable, "窗口1");
t1.start();
Thread t2 = new Thread(runnable, "窗口2");
t2.start();
Thread t3 = new Thread(runnable, "窗口3");
t3.start();
}
}
3.同步锁
Lock锁也称同步锁。在需要同步的代码块开始前加lock(),结束位置unlock()释放锁。锁机制更加强大与灵活(比如可以观察到线程是否获得了锁)
格式:
public void lock() //加同步锁。
//需要同步的代码
public void unlock() //释放同步锁。
使用方式类似于synchronized同步代码块。
三,线程池
1.线程池的集中创建方式
线程池创建有七种方式,最核心的是最后一种:
newSingleThreadExecutor():它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目;
newCachedThreadPool():它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 SynchronousQueue 作为工作队列;
newFixedThreadPool(int nThreads):重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads;
newSingleThreadScheduledExecutor():创建单线程池,返回 ScheduledExecutorService,可以进行定时或周期性的工作调度;
newScheduledThreadPool(int corePoolSize):和 newSingleThreadScheduledExecutor() 类似,创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程;
newWorkStealingPool(int parallelism):这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 ForkJoinPool,利用 Work-Stealing 算法,并行地处理任务,不保证处理顺序;
ThreadPoolExecutor():是最原始的线程池创建,上面 1-3 创建方式都是对 ThreadPoolExecutor 的封装。
2.线程池的构造参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler);
corePoolSize: 线程池核心线程的数量;
maximumPoolSize: 线程池可创建的最大线程数量;
keepAliveTime: 当线程数量超过了 corePoolSize 指定的线程数,并且空闲线程空闲的时间达到当前参数指定的时间时该线程就会被销毁,如果调用过 allowCoreThreadTimeOut(boolean value)方法允许核心线程过期,那么该策略针对核心线程也是生效的;
unit: 指定了 keepAliveTime 的单位,可以为毫秒,秒,分,小时等;
workQueue: 存储未执行的任务的队列;
threadFactory: 创建线程的工厂,如果未指定则使用默认的线程工厂;
handler: 指定了当任务队列已满,并且没有可用线程执行任务时对新添加的任务的处理策略。
3.线程池的调度策略
当初始化一个线程池之后,池中是没有任何用户执行任务的活跃线程的,当新的任务到来时,根据配置的参数其主要的执行任务如下:
若线程池中线程数小于 corePoolSize 指定的线程数时,每来一个任务,都会创建一个新的线程执行该任务,无论线程池中是否已有空闲的线程;
若当前执行的任务达到了 corePoolSize 指定的线程数时,也即所有的核心线程都在执行任务时,此时来的新任务会保存在 workQueue 指定的任务队列中;
当所有的核心线程都在执行任务,并且任务队列中存满了任务,此时若新来了任务,那么线程池将会创建新线程执行任务;
若所有的线程(maximumPoolSize 指定的线程数)都在执行任务,并且任务队列也存满了任务时,对于新添加的任务,其都会使用 handler 所指定的方式对其进行处理。
4.线程池的状态
RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法terminated()。
TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
5.线程池中 submit() 和 execute() 方法有什么区别
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
6.Callable接口+线程池的调用
- 利用线程池启动线程
- Future接口接受返回对象,调用多线程submit方法;
- 调用future.get()方法接受返回值
- 关闭线程池
四,线程的生命周期及状态
1.线程的生命周期
2.查看线程状态
使用thread.getState()方法可以查看线程当前的状态。
五,多线程的三大特性
1.三大特性
- 原子性:一个操作或者一系列操作,要么全部执行成功,要么全部执行失败;
- 可见性:当一个线程修改了某些线程共享变量的值,其他线程能够立即得知这个修改;
- 有序性:保证指令不会受 cpu 指令并行优化的影响(CPU在执行指令时,编译器和处理器可以对指令进行重排序,重排序过程不会影响到单线程的执行,却会影响到多线程并发执行的结果。保证有序性即保证程序执行的顺序按照代码的先后顺序执行)。
2.如何保证线程安全
- Synchronize关键字
修饰类、方法、代码段的关键字。保证只有一个线程拿到锁,进入同步代码块操作共享资源,临界区内的代码对外是不可分割的,不会被线程切换打断,保证了原子性;
执行sync锁住的代码前,会先执行lock,unlock操作,同步更新变量的值,保证对任意共享变量的操作都是对其他线程可见的, 实现了可见性;
sync内的代码和外部的代码禁止排序,至于内部的代码,不会禁止排序,但是由于只有一个线程能进入同步代码块,相当于在代码块中是单线程的,即使代码块中发生了重排序,也不会影响程序执行的结果。
- volatile关键字
修饰变量的关键字。
volatile作用有两个:
一,保证特定操作的执行顺序,保证有序性,禁止指令重排;
二,保证某些变量的内存可见性。
保证可见性,被volatile修饰的变量,一旦被线程修改其他线程锁保持的该变量的缓存就会失效,需要从内存中重新获取最新值,也叫做MESI缓存一致性协议。
volatile无法保证原子性。
- atomic原子类
一种乐观锁,用atomic原子类替代基本数据类型,例如用incrementAndGet方法代替a++。无论原子更新哪种类型,都要遵循比较和替换的原则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败,保证了原子性。
总结:
可见性 | 有序性 | 原子性 | |
---|---|---|---|
synchronize | 可以 | 可以 | 可以 |
volatile | 可以 | 可以 | 不可以 |
atomic | 可以 | 不可以 | 可以 |
六,死锁
1.什么是死锁
当线程 A 持有独占锁 a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁 a 的情况下,就会发生 AB 两个线程由于互相持有对方需要的锁,而发生的阻塞现象,我们称为死锁。
简单来说,就是两个线程为了争抢资源而发生的相互等待的线程阻塞现象。
2.死锁产生的原因
- 互斥条件:每个资源同时只能被一个线程使用。(专一性)
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源,在使用完之前不可被强行剥夺。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求,若干线程形成了一种头尾相接,循环等待资源的恶性循环等待关系。
上面列出了死锁的四个必要条件,只要打破其中的任意一个或多个条件就可以避免死锁发生。
3.处理死锁的办法
预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
4.怎么防止死锁
1、尽量使用 tryLock(long timeout, TimeUnit unit)的方法(ReentrantLock、ReentrantReadWriteLock),设置超时时间,超时可以退出防止死锁。
2、尽量使用 Java. util.concurrent 并发类代替自己手写锁。
3、尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
4、尽量减少同步的代码块。
5.检测与解除死锁
一般来说,由于操作系统有并发,共享以及随机性等特点,通过预防和避免的手段达到排除死锁的目的是很困难的。这需要较大的系统开销,而且不能充分利用资源。为此,一种简便的方法是系统为进程分配资源时,不采取任何限制性措施,但是提供了检测和解脱死锁的手段:能发现死锁并从死锁状态中恢复出来。因此,在实际的操作系统中往往采用死锁的检测与恢复方法来排除死锁。
- 资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。
- 撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
- 进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。