一、进程(process)与线程(Thread)
线程是CPU调度和执行的单位。
-
程序是指令和数据的有序集合,其本身并没有任何运行的含义,只是一个静态的概念。
-
进程是指程序的一次执行过程(一个程序运行起来即是一个进程),它是一个动态的概念(相对于程序来说是动态的),是系统资源分配的单位,是线程的一个保护伞。
进程中多个线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序是不能人为干预的。
-
线程是在进程中的,一个进程中可以包含若干个线程,但是一个进程中至少有一个线程。
线程是独立的执行路径。
在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程(main()函数)、gc线程。
对同一份资源操作时可能会存在资源抢夺的问题,这时就需要加入并发控制。线程会带来额外的开销,比如CPU调度时间、并发控制开销等。
每个线程在自己的工作内存交互,内存控制不当会造成数据的不一致。
-
补充:
由于需要限制不同程序之间的访问能力,防止他们获取别的程序的内存数据,或者获取外围设备的数据并发送到网络,CPU划分出两个权限等级:用户态和内核态。
- 内核态:CPU可以访问内存所有数据,包括外围设备,比如硬盘、网卡、CPU也可以将自己从一个程序切换到另一个程序;
- 用户态:只能受限的访问内存,且不允许访问外围设备,占用CPU的能力被剥夺,CPU资源可以被其他程序获取。
注意:很多线程是模拟出来的,真正的多线程是指有多个CPU,也就是多核,比如说服务器。如果是模拟出来的多线程,也就是在一个CPU的情况下,在同一个时间点,CPU只能执行一个代码,因为切换的会很快,所以就有同时执行的错觉。
二、线程的三种创建方式
线程有三种创建方式:继承Thread类、实现Runable接口、实现Callable接口,三种方法都是调用 start() 方法来启动一个线程的。
线程开启不一定立即执行,由CPU调度执行。
run() 方法和 start() 方法区别:
-
调用 run() 方法相当于方法的调用。在执行时会先调用 run() 方法再执行 main() 方法。
-
调用 start() 方法表示开启了一个线程。main() 方法继续执行,run() 方法同时执行。
2.1 继承Thread类
-
步骤:
-
自定义线程类并继承Thread类;
-
重写run()方法,编写线程执行体;
-
创建线程对象,调用start()方法启动线程。
不建议使用,避免OOP单继承局限性。
-
-
demo
/** * @author QHJ * @date 2022/7/27 09:15 * @description: 继承Thread类 */ // 继承Thread类、重写run()方法、调用start()开启线程 // 注意:线程开启不一定立即执行,由CPU调度执行 public class ThreadTest1 extends Thread { public static void main(String[] args) { // 创建一个线程对象并调用start()方法开启线程 ThreadTest1 threadTest = new ThreadTest1(); // 两条线程同时执行 threadTest.start(); // 先执行run()方法线程,再执行主方法线程 // 先执行“我在写代码”,再执行"我在学习多线程“ // threadTest.run(); // main()线程主线程 for (int i = 0; i < 5; i++){ System.out.println("我在学习多线程" + i); } } @Override public void run() { // run()方法线程体 for (int i = 0; i < 5; i++){ System.out.println("我在写代码" + i); } } }
-
多线程同步下载图片
/** * @author QHJ * @date 2022/7/27 10:32 * @description: 多线程同步下载图片 */ public class ThreadTest2 extends Thread{ private String url; private String name; public ThreadTest2(String url, String name) { this.url = url; this.name = name; } // 下载图片线程的执行体 @Override public void run() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载图片的名称为:" + name); } public static void main(String[] args) { ThreadTest2 threadTest21 = new ThreadTest2("https://img2.baidu.com/it/u=3004838856,884847121&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281", "1.jpg"); ThreadTest2 threadTest22 = new ThreadTest2("https://img2.baidu.com/it/u=4084621093,2971972319&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500", "2.jpg"); ThreadTest2 threadTest23 = new ThreadTest2("https://img0.baidu.com/it/u=625725529,2244476940&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500", "3.jpg"); threadTest21.start(); threadTest22.start(); threadTest23.start(); } } // 下载器 class WebDownloader{ // 下载方法 public void downloader(String url, String name){ try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); } } }
2.2 实现Runable接口
-
步骤:
- 创建线程类实现Runable接口;
- 启动线程:new Thread(目标对象).start()。
推荐使用,避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。
-
练习demo
/** * @author QHJ * @date 2022/7/27 11:06 * @description: 实现Runable接口 */ public class ThreadTest3 implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++){ System.out.println("我在学习多线程" + i); } } public static void main(String[] args) { // 创建runable接口的实现类对象 ThreadTest3 threadTest3 = new ThreadTest3(); // 创建线程对象,通过线程对象来开启我们的线程,起到代理作用 // Thread thread = new Thread(threadTest3); // thread.start(); new Thread(threadTest3).start(); for (int i = 0; i < 10; i++){ System.out.println("我在写代码" + i); } } }
-
火车票抢票
/** * @author QHJ * @date 2022/7/27 11:17 * @description: 多个线程同时操作同一个对象 */ // 火车票例子=>引发并发问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱。 public class ThreadTest4 implements Runnable{ // 票数 private int ticketNums = 10; @Override public void run() { while (true){ if (ticketNums <= 0){ break; } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票"); } } public static void main(String[] args) { // 一份资源 ThreadTest4 threadTest4 = new ThreadTest4(); // Thread是线程的代理对象来开启线程 多个代理 new Thread(threadTest4, "张三").start(); new Thread(threadTest4, "李四").start(); new Thread(threadTest4, "王五").start(); } }
结果会出现两个人同时抢到同一张票,引发并发问题。
-
龟兔赛跑
/** * @author QHJ * @date 2022/7/27 13:39 * @description: 龟兔赛跑 */ public class Race implements Runnable{ // 胜利者 private static String winner; public static void main(String[] args) { Race race = new Race(); new Thread(race, "兔子").start(); new Thread(race, "乌龟").start(); } @Override public void run() { // 设定距离为100 for (int i = 0; i <= 100; i++){ boolean flag = gameOver(i); if (flag){ break; } System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步"); } } // 判断比赛是否结束 private boolean gameOver(int steps){ // 如果已经有胜利者了 if (winner != null){ return true; }else{ if (steps >= 100){ winner = Thread.currentThread().getName(); System.out.println(winner + "是胜利者"); return true; } } return false; } }
2.3 实现Callable接口(了解)
-
步骤:
- 创建线程类实现Callable接口,需要返回值类型(默认是Object类型);
- 重写call方法,需要抛出异常;
- 创建目标对象(数量是n);
- 创建执行服务:ExecutorService service = Executors.newFixedThreadPool(n);
- 提交执行:Future< Boolean > r1 = service.submit(threadTest51);
- 获取结果:boolean reaults1 = r1.get();
- 关闭服务:service.shutdown()。
-
练习demo
package cn.qhj.thread; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.concurrent.*; /** * @author QHJ * @date 2022/7/27 14:02 * @description: 实现Callable接口 */ // 1.创建线程实现Callable接口 public class ThreadTest5 implements Callable { private String url; private String name; public ThreadTest5(String url, String name) { this.url = url; this.name = name; } // 2.重写call()方法 @Override public Object call(){ WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url, name); System.out.println("下载图片的名称为:" + name); return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 3.创建目标对象 ThreadTest5 threadTest51 = new ThreadTest5("https://img2.baidu.com/it/u=3004838856,884847121&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=281", "1.jpg"); ThreadTest5 threadTest52 = new ThreadTest5("https://img2.baidu.com/it/u=4084621093,2971972319&fm=253&fmt=auto&app=120&f=JPEG?w=889&h=500", "2.jpg"); ThreadTest5 threadTest53 = new ThreadTest5("https://img0.baidu.com/it/u=625725529,2244476940&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500", "3.jpg"); // 4.创建执行服务 ExecutorService service = Executors.newFixedThreadPool(3); // 5.提交执行 Future<Boolean> r1 = service.submit(threadTest51); Future<Boolean> r2 = service.submit(threadTest52); Future<Boolean> r3 = service.submit(threadTest53); // 6.获取结果 boolean reaults1 = r1.get(); boolean reaults2 = r2.get(); boolean reaults3 = r3.get(); System.out.println(reaults1); System.out.println(reaults2); System.out.println(reaults3); // 7.关闭服务 service.shutdown(); } // 下载器 class WebDownloader{ // 下载方法 public void downloader(String url, String name){ try { FileUtils.copyURLToFile(new URL(url), new File(name)); } catch (IOException e) { e.printStackTrace(); } } } }
三、静态代理
所谓代理就是指帮别人做事情。比如“我要结婚”这个事情,婚礼会请婚庆公司来帮忙操办,而自己又可以做别的事情了。
实现步骤:
- 在接口中创建要实现的方法;
- 目标对象实现接口,自己实现功能(做自己的事情);
- 代理对象实现接口,帮助目标对象实现功能(帮助目标对象做一些事情);
- 通过代理类调用要实现的方法实现功能。
这个 WeddingCompany 类就是一个代理类,它帮助You对象做婚礼操办(调用 HappyMarry() 方法);
多线程中的 Thread类 与 WeddingCompany 的角色类似,通过实现 Runable 接口来做代理操作(调用Start()方法)。
/**
* @author QHJ
* @date 2022/7/27 15:09
* @description: 静态代理模式
* 目标对象和代理对象都要实现同一个接口;
* 代理对象要代理真实角色;
* 代理对象可以做很多真实对象做不了的事情,真实对象专注于做自己的事情
*/
public class StaticProxy {
public static void main(String[] args) {
new WeddingCompany(new You()).HappyMarry();
new Thread(() -> System.out.println("我爱你")).start();
// You you = new You();
// WeddingCompany weddingCompany = new WeddingCompany(you);
// weddingCompany.HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
// 真实角色,自己结婚
class You implements Marry{
@Override
public void HappyMarry() {
// 真实对象只专注于做自己的事情
System.out.println("今天我要结婚了,真开心(*^▽^*)");
}
}
// 代理角色,帮助别人结婚
class WeddingCompany implements Marry{
// 代理谁?真实目标角色
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
// 代理对象需要帮助真实对象做很多事情
@Override
public void HappyMarry() {
before();
this.target.HappyMarry();
after();
}
private void before(){
System.out.println("结婚之前布置现场");
}
private void after(){
System.out.println("结婚之后收尾款");
}
}
四、线程的状态
4.1 线程的几种状态
线程的五个状态:创建、就绪、阻塞、运行、死亡。
线程中断或者结束,一旦进入死亡状态就不能再次启动。
线程可以处于以下状态之一:
runnable:将线程扔到CPU等待队列中,在等待队列等着让CPU运行,在运行时候的状态叫 running;
yield:调用的时候从 running 回到 runnable;
block:加了 synchronized 但是没有获得锁的时候会阻塞,获得锁之后就进入到就绪状态;
waiting:运行时调用 join()、wait()、park()进入waiting状态;调用 notify()、notifyAll()、unpark()进入runnable状态;
timedwaiting:调用 sleep()、wait()、join(time)进入 timedwaiting 状态,时间结束后自己回去;
线程挂起:单核CPU会执行很多的线程,但是在一个时间点内只能执行一个,所以线程就要切换执行,把一个线程扔出去的过程就是线程挂起。
/**
* @author QHJ
* @date 2022/7/27 21:27
* @description: 测试线程的状态
*/
public class StateTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这是线程执行的时候输出的内容.");
}
});
// 观察状态
// 1.新建状态(尚未启动)
Thread.State state = thread.getState();
System.out.println("这是新建线程的状态:" + state); // NEW
// 2.就绪状态(在java虚拟机中执行)
thread.start();
state = thread.getState();
System.out.println("这是线程的就绪状态:" + state); // RUNNABLE
// 3.运行状态(线程尚未完成)
while (state != Thread.State.TERMINATED){
Thread.sleep(600);
state = thread.getState();
System.out.println("这是线程正在运行的状态:" + state); // TIMED_WAITING、TERMINATED
}
}
}
4.2 线程停止(stop)
-
不推荐使用 JDK 提供的 stop()、destory()方法;【已废弃】
-
推荐让线程自己停下来;
-
建议使用一个标志位进行终止变量:当 flag=false 时终止线程运行。
让线程正常结束就是关闭线程,等线程执行完毕了就会自己移出去。stop() 方法太粗暴不建议使用,容易产生状态的不一致。
/**
* @author QHJ
* @date 2022/7/27 17:08
* @description: 强制停止线程
*/
public class StopTest implements Runnable{
// 1.设置一个标识位
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("run...Thread线程正在运行" + i++);
}
}
// 2.设置一个公开的方法停止线程,转换标识位
public void stop(){
this.flag = false;
}
public static void main(String[] args) {
StopTest stopTest = new StopTest();
new Thread(stopTest).start();
for (int i = 0; i < 1000; i++){
System.out.println("main" + i);
if (i == 900){
// 调用stop方法切换标志位,让线程停止
stopTest.stop();
System.out.println("线程停止了.");
}
}
}
}
4.3 线程休眠(sleep)
sleep(时间)指定当前线程阻塞的毫秒数;
sleep存在异常 InterruptedException;
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时、倒计时等;
每一个对象都有一个锁(在线程同步中使用),sleep不会释放锁。
-
网络延时
/** * @author QHJ * @date 2022/7/27 17:42 * @description: 模拟网络延时:放大问题的发生性 */ public class SleepTest implements Runnable{ // 票数 private int ticketNums = 10; @Override public void run() { while (true){ if (ticketNums <= 0){ break; } // 模拟延时 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票"); } } public static void main(String[] args) { // 一份资源 ThreadTest4 threadTest4 = new ThreadTest4(); // Thread是线程的代理对象来开启线程 多个代理 new Thread(threadTest4, "张三").start(); new Thread(threadTest4, "李四").start(); new Thread(threadTest4, "王五").start(); } }
-
倒计时
/** * @author QHJ * @date 2022/7/27 17:45 * @description: 模拟倒计时 */ public class SleepTest2 { public static void main(String[] args){ // 获取系统当前时间 Date startTime = new Date(System.currentTimeMillis()); while (true){ try { Thread.sleep(1000); System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime)); // 更新当前时间 startTime = new Date(System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } } } public void timeDown() throws InterruptedException { int num = 10; while (true){ Thread.sleep(1000); System.out.println(num--); if (num <= 0){ break; } } } }
4.4 线程礼让(yield)
礼让线程,让当前正在执行的线程暂停,但不阻塞;
将线程从运行状态转为就绪状态(进入等待队列中),从 running 回到 runnable;
让CPU重新调度,礼让不一定成功,看CPU心情(CPU再次调度的时候很有可能把刚刚加入等待队列的再拿回去,yield只是让出一下,至于别的线程能不能抢得到就另说了)。
/**
* @author QHJ
* @date 2022/7/27 18:03
* @description: 线程礼让
*/
public class YieldTest {
public static void main(String[] args) {
MyYield myYield = new MyYield();
new Thread(myYield, "a").start();
new Thread(myYield, "b").start();
}
}
class MyYield implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程开始执行");
// 礼让
Thread.yield();
System.out.println(Thread.currentThread().getName() + "线程停止执行");
}
}
4.5 线程合并(join)
join 合并线程,待此线程执行完成后,再执行其他线程,其他线程处于阻塞状态。可以想象成插队。
/**
* @author QHJ
* @date 2022/7/27 20:45
* @description: 线程合并
*/
public class JoinTest implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++){
System.out.println("线程vip来了" + i);
}
}
public static void main(String[] args) throws InterruptedException {
// 启动线程
JoinTest joinTest = new JoinTest();
Thread thread = new Thread(joinTest);
thread.start();
// 主线程
for (int i = 0; i < 500; i++){
// 插队
if (i == 200){
// 当 i==200时,joinTest线程插队直至完全执行完毕后主线程再执行
thread.join();
}
System.out.println("main" + i);
}
}
}
4.6 线程中断
interupt 打断。线程被打断之后会抛出异常、中断线程。中断之后可以使用 Thread.currentThread().isInterrupt() 方法检测到线程是否被中断,在 sleep() 的时候会抛出中断异常。
/**
* @author QHJ
* @date 2022/7/29 14:22
* @description: 线程中断
*/
public class InteruptTest {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("我想要这个线程中断");
myThread.interrupt();
if (myThread.isInterrupted()) {
System.out.println("线程中断了");
}
}
}
class MyThread extends Thread{
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.7 线程优先级(PRIORITY)
优先级越高越先被执行。
Java提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
线程优先级的设定一般在start()之前,用数字表示,范围从 1~10:
- Thread.MIN_PRIORITY = 1;
- Thread.MAX_PRIORITY = 10;
- Thread.NORM_PRIORITY = 5(默认).
改变或获取优先级的方法:gtPriority()和setPriority(int xxx)。
注意:优先级只是意味着获得调度的概率,并不是优先级低就不会被调用了,这都是要看CPU的调度的。
性能倒置问题:优先级低的比优先级高的先执行了(一般不会有这个问题的)。
/**
* @author QHJ
* @date 2022/7/28 06:15
* @description: 线程优先级测试
*/
public class PriorityTest{
public static void main(String[] args) {
System.out.println("这是主线程的优先级:" + Thread.currentThread().getName() + Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread thread1 = new Thread(myPriority);
Thread thread2 = new Thread(myPriority);
Thread thread3 = new Thread(myPriority);
Thread thread4 = new Thread(myPriority);
Thread thread5 = new Thread(myPriority);
// 在启动线程之前先设定优先级
thread1.setPriority(7);
thread1.start();
thread2.start();
thread3.setPriority(2);
thread3.start();
thread4.setPriority(Thread.NORM_PRIORITY);
thread4.start();
thread5.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println("这是其他线程的优先级:" + Thread.currentThread().getName() + "->" + Thread.currentThread().getPriority());
}
}
4.8守护(daemon)线程
线程分为用户线程
和守护线程
。
虚拟机必须确保用户线程执行完毕(main()线程)。
虚拟机不用等待守护线程执行完毕(gc()线程),但是在用户线程执行完毕后不能立即停止,需要一点停止缓冲时间。
守护线程的作用:后台记录操作日志、监控内存、垃圾回收等待等等。
设置守护线程的方法:setDaemon(boolean),默认是false表示是用户线程。
/**
* @author QHJ
* @date 2022/7/28 06:48
* @description: 守护线程测试
*/
public class DaemonTest {
public static void main(String[] args) {
You you = new You();
God god = new God();
// 上帝是守护线程
Thread thread = new Thread(god);
thread.setDaemon(true); // 默认是false表示是用户线程
thread.start();
new Thread(you).start();
}
}
// 上帝(守护线程)
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("上帝守护着你~");
}
}
}
// 你
class You implements Runnable{
@Override
public void run() {
for (int i = 0; i < 36500; i++){
System.out.println("你一直开心的活着(*^▽^*)");
}
System.out.println("Say goodbye to the worldo(╥﹏╥)o");
}
}
五、线程同步
5.1 线程同步概述
线程同步是指多个线程操作同一个资源。处理多线程问题时,多线程访问同一个对象(并发),并且某线程还想修改这个对象,这个时候就需要线程同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池
形成队列,等待前面线程使用完毕,下一个线程再使用。
并发:同一个对象被多个线程同时操作,比如:抢票系统、取钱(银行和手机同时取钱)等。
由于同一进程的多个线程共享同一块存储空间,在带来方便的同时也带来了访问冲突的问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized):当一个线程获得对象的排它锁,独占资源,其他线程就必须等待,使用后释放锁就可以了。但是会存在一些问题:
-
一个线程持有锁会导致其他所有需要此锁的线程挂起;
-
在多线程竞争下加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
-
如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题(性能倒置)。
比如:在上厕所排队时,小号的优先级高于大号的优先级,但是大号先拿到了锁,这样就会导致小号以及后面排队的人需要更长的等待时间,很明显效率降低了。
线程同步的形成条件:队列+锁(synchronized)。
这个条件为了保证线程的安全性,解决线程不安全带来的问题。但是会损失程序的性能。
5.2 三大不安全案例
-
抢票
/** * @author QHJ * @date 2022/7/28 09:58 * @description: 抢票不安全问题 */ public class UnsafeBuyTicket { public static void main(String[] args) { BuyTickets buyTickets = new BuyTickets(); new Thread(buyTickets, "张三").start(); new Thread(buyTickets, "李四").start(); new Thread(buyTickets, "王五").start(); new Thread(buyTickets, "黄牛").start(); } } // 购买类 class BuyTickets implements Runnable { private int ticketNums = 10; boolean flag = true; @Override public void run() { while (flag){ buy(); } } // 购买的方法 private void buy(){ if (ticketNums < 1){ flag = false; return; } try { // 模拟延时 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票!"); } }
-
银行取钱问题
/** * @author QHJ * @date 2022/7/28 11:01 * @description: 银行取钱问题 */ public class UnsafeBank { public static void main(String[] args) { // 账户 Account account = new Account(1000, "基金"); Drawing her = new Drawing(account, 520,"旺仔"); Drawing me = new Drawing(account, 520,"牛奶"); her.start(); me.start(); } } // 账户 class Account{ int money; String name; public Account(int money, String name) { this.money = money; this.name = name; } } // 模拟取款 class Drawing extends Thread{ // 账户 Account account; // 取了多少钱 int drawingMoney; // 现在手里有多少钱 int nowMoney; public Drawing(Account account, int drawingMoney, String name){ super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { // 判断有没有钱 if (account.money - drawingMoney < 0){ System.out.println(Thread.currentThread().getName() + "钱不够了,取不了哦"); return; } try { // 模拟延时,放大问题的发生性 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 卡内余额 account.money = account.money - drawingMoney; // 手里的钱 nowMoney = nowMoney + drawingMoney; // 卡内余额 System.out.println(account.name + "卡内余额为:" + account.money); // 手里余额 this.getName() = Thread.currentThread().getName() 继承了Thread类,可以使用Thread类里的所有方法 System.out.println(this.getName() + "手里的钱为:" + nowMoney); } }
-
线程不安全问题
/** * @author QHJ * @date 2022/7/28 13:25 * @description: 线程不安全问题 */ public class UnsafeList { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 10000; i++){ new Thread(() -> { list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } }
5.3 同步方法
-
我们可以通过 private 关键字来保证数据对象只能被方法访问,所以只需要针对方法提出一套机制:synchronized关键字。它包括两种用法:synchronized 方法和 synchronized 块。
同步方法:public synchronized void method(int args){ }
-
synchronized 方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获取调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁继续执行。
同步方法的缺陷:如果将一个大的方法声明为 synchronized 将会影响效率。
synchronized 加在方法上,如果方法里面还有别的业务逻辑,在加锁的时候,能锁的少的尽量锁的少,粒度要小一些。
// 购买的方法,使用synchronized同步方法,锁的是this:指的是BuyTickets2类
private synchronized void buy(){
if (ticketNums < 1){
flag = false;
return;
}
try {
// 模拟延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票!");
}
5.4 同步块
同步块:synchronized(Obj){ 操作 } 其中锁的 Obj(对象)就是变化的量,需要增删改的量。
-
Obj 称为同步监视器:
- 它可以是任何对象,但是推荐使用共享资源作为同步监视器;
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是 this,就是这个对象本身或者是class(反射)。
-
同步监视器的执行过程:
- 第一个线程访问,锁定同步监视器,执行其中的代码;
- 第二个线程访问,发现同步监视器被锁定,无法访问;
- 第一个线程访问完毕,解锁同步监视器;
- 第二个线程访问,发现同步监视器没有锁,然后锁定并访问。
// 在方法上锁定,synchronized默认锁的是this,这里的this指的是Drawing2,不是想要的可变对象
@Override
public void run() {
// synchronized块锁的就是变化的量,也就是需要增删改的量
// synchronized块,锁的是账户account
synchronized (account){
// 判断有没有钱
if (account.money - drawingMoney < 0){
System.out.println(Thread.currentThread().getName() + "钱不够了,取不了哦");
return;
}
try {
// 模拟延时,放大问题的发生性
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额
account.money = account.money - drawingMoney;
// 手里的钱
nowMoney = nowMoney + drawingMoney;
// 卡内余额
System.out.println(account.name + "卡内余额为:" + account.money);
// 手里余额 this.getName() = Thread.currentThread().getName() 继承了Thread类,可以使用Thread类里的所有方法
System.out.println(this.getName() + "手里的钱为:" + nowMoney);
}
}
5.5 synchronized关键字
多个线程去访问同一个资源的时候要给资源上锁,访问一段代码或者某个临界资源的时候就需要一把锁。
线程去访问对象的时候要看锁是不是属于该对象(此锁是不是被该对象占有),如果属于该对象才能对对象做操作。
-
sync底层
JVM 规范没有做要求。
hotspot 实现:在对象的头64位上拿出2位来记录这个对象是不是被锁定了 markword,2位的组合分别是不同锁的类型。
-
锁升级
JDK早期,重量级的 sync 要去找操作系统申请锁,然后就会造成 sync 效率非常低,后来做了改进。
sync hotspot 实现:上来后先访问某把锁的线程。
sync(obj):现在 obj 的 markword 头上记录这个线程 id(偏向锁)没有加锁,默认不会有第二个线程抢这把锁,如果还是这个线程来访问,也不用申请锁,而是直接开始执行,效率非常高。如果有线程争用,就升级为自旋锁(比如:你在马桶上,有个人也要用马桶,在旁边等着,但不会进入就绪队列,会一直转圈,转了很久之后发现你没有出来时锁就会进一步升级),自旋锁默认的情况下旋10次,10次之后如果还得不到锁,就升级为重量级锁去操作系统申请资源。自旋的时候会消耗CPU,10次之后自旋的线程就到了等待队列,不消耗CPU,锁只能升级不能降级。
-
自旋锁
自旋锁占用CPU,但是不访问操作系统(os),所以是在用户态解决锁的问题,不经过内核态,因此
在效率上要比经过内核态效率要高。自旋锁由于要占用CPU,os 锁不占CPU,所谓的不占用CPU是说旁边
竞争的线程进入等待队列等着不占用CPU,直到CPU让你运行了,你才会被叫起来**。所以执行时间长,线程数比较多的用os锁,加锁代码执行时间短,线程还不能太多就用自旋锁**,如果线程太多 比如说10000个自旋,一个线程执行 9999个自旋,也是很消耗CPU的 。
六、死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,以致都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
// 某人既想拿到口红也想拿到镜子,但是拿到镜子的那个人也有这个想法,双方都拿着对方需要的资源不放,就造成了死锁
// 获得口红的锁
synchronized (lipsLick){
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
// 死锁
synchronized (mirror){
// 一秒钟后想要获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
}
}
-
产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个进程使用;
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
- 不剥夺条件:进程已获得的资源,在未使用完之前不能强行剥夺;
- 循环等待条件:若干个进程之间形成一种头尾相接的循环等待资源关系。
-
避免死锁的方法
死锁的四个必要条件中只要想办法破坏其中任意一个或者多个条件就可以避免死锁的发生。
// 获得口红的锁 synchronized (lipsLick){ System.out.println(this.girlName + "获得口红的锁"); Thread.sleep(1000); // 死锁 /*synchronized (mirror){ // 一秒钟后想要获得镜子的锁 System.out.println(this.girlName + "获得镜子的锁"); }*/ } // 不让它抱对方的锁 synchronized (mirror){ // 一秒钟后想要获得镜子的锁 System.out.println(this.girlName + "获得镜子的锁"); }
七、Lock锁
-
Lock锁
-
从 JDK5.0 开始,Java提供了更强大的线程同步机制—通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
-
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。
-
ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。
-
-
synchronized 与 Lock 的对比
- Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁)。synchronized 是隐式锁,除了作用于自动释放。
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁。
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
- 优先使用顺序:Lock > 同步代码块(已经进入了方法体,分配了相应资源) > 同步方法(在方法体之外)
package cn.qhj.lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author QHJ
* @date 2022/7/28 16:21
* @description: Lock锁
*/
public class LockTest {
public static void main(String[] args) {
lockTest2 lock = new lockTest2();
new Thread(lock, "张三").start();
new Thread(lock, "李四").start();
new Thread(lock, "王五").start();
new Thread(lock, "黄牛").start();
}
}
// 购买类
class lockTest2 implements Runnable {
private int ticketNums = 10;
boolean flag = true;
// 定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (flag){
buy();
}
}
// 购买的方法
private void buy(){
try {
// 加锁
lock.lock();
if (ticketNums < 1){
flag = false;
return;
}
try {
// 模拟延时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNums-- + "张票!");
}finally {
// 解锁
lock.unlock();
}
}
}
八、线程协作
-
线程通信
-
应用场景:生产者和消费者问题
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费;
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止;
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
-
分析
这是一个线程同步问题,生产者和消费者共享同一种资源,并且生产者和消费者之间相互依赖,互为条件。
对于生产者,没有生产产品之前,要通知消费者等待。而生产了产品之后又需要马上通知消费者消费;
对于消费者,在消费之后要通知生产者已经结束消费,需要生产新的产品以供消费。
在生产者消费者问题中,仅有 synchronized 是不够的:
synchronized可阻止并发更新同一个共享资源,实现了同步;
synchronized不能用来实现不同线程之间的消息传递(通信)。
-
解决方式1—管程法
并发协作模型“生产者/消费者模式”—管程法:
生产者:负责生产数据的模块(可能是方法、对象、线程、进程);
消费者:负责处理数据的模块(可能是方法、对象、线程、进程);
缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”。
生产者将生产好的数据放入缓冲区,消费者从缓冲区中拿数据。
-
解决方式2—信号灯法
并发协作模型“生产者/消费者模式”—信号灯法
通过标志位来判断。
-
-
管程法
package cn.qhj.comm; /** * @author QHJ * @date 2022/7/28 17:51 * @description: 管程法(生产者、消费者、产品、缓冲区) */ public class PCTest { public static void main(String[] args) { SynContainer container = new SynContainer(); new Productor(container).start(); new Consumer(container).start(); } } // 生产者 class Productor extends Thread{ SynContainer container; public Productor(SynContainer container){ this.container = container; } // 生产 @Override public void run() { for (int i = 0;i < 100; i++){ container.push(new Subject(i)); System.out.println("生产了" + i + "个产品"); } } } // 消费者 class Consumer extends Thread{ SynContainer container; public Consumer(SynContainer container){ this.container = container; } // 消费 @Override public void run() { for (int i = 0;i < 100; i++){ Subject subject = container.pop(); System.out.println("消费了" + subject.getId() + "个产品"); } } } // 产品 class Subject{ // 产品编号 private int id; public Subject(int id) { this.id = id; } public int getId() { return id; } public void setId(int id) { this.id = id; } } // 缓冲区 class SynContainer{ // 需要一个容器的大小 Subject[] subjects = new Subject[10]; // 容器计数器 int count = 0; // 生产者放入产品 public synchronized void push(Subject subject){ // 如果容器满了 if (count == subjects.length){ // 通知生产者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果没有满,放入产品 subjects[count] = subject; count++; // 通知消费者消费 this.notifyAll(); } // 消费者消费产品 public synchronized Subject pop(){ // 判断能否消费 if (count == 0){ // 等待生产者生产 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果可以消费 count--; Subject subject = subjects[count]; // 消费完后通知生产者生产 this.notifyAll(); return subject; } }
-
信号灯法
package cn.qhj.comm; /** * @author QHJ * @date 2022/7/29 06:45 * @description: 信号灯法(通过标志位解决) */ public class PCTest2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Watcher(tv).start(); } } // 生产者——演员 class Player extends Thread{ TV tv; public Player(TV tv){ this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++){ if (i % 2 == 0){ this.tv.play("快乐大本营"); }else { this.tv.play("抖音:记录美好生活"); } } } } // 消费者——观众 class Watcher extends Thread{ TV tv; public Watcher(TV tv){ this.tv = tv; } @Override public void run() { for (int i = 0; i < 20; i++){ this.tv.watch(); } } } // 产品——节目 class TV{ // 演员表演,观众等待 T // 观众观看,演员等待 F // 表演的节目 String voice; // 标志位 boolean flag = true; // 表演 public synchronized void play(String voice){ // 如果为false,则演员等待 if (!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("演员表演了" + voice + "节目"); // 通知观众观看 this.notifyAll(); this.voice = voice; this.flag = !this.flag; } // 观看 public synchronized void watch(){ // 如果为true,则观众等待 if (flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观众观看了" + voice + "节目"); // 通知演员表演 this.notifyAll(); this.voice = voice; this.flag = !this.flag; } }
九、线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似于生活中的公共交通工具。
好处:提高响应速度(减少了创建新线程的时间)、降低资源消耗(重复利用线程池中线程,不需要每次都创建)、便于线程管理。
-
使用线程池
JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors。
- ExecutorService:真正的线程池接口,常见的子类有 ThreadPoolExecutor
- viod execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable;
- < T >Future< T > submit(Callable< T > task):执行任务,有返回值,一般用来执行Callable;
- void shutdown():关闭连接池。
- Executors:工具类、线程池的共产类,用于创建并返回不是同类型的线程池。
- ExecutorService:真正的线程池接口,常见的子类有 ThreadPoolExecutor
package cn.qhj.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author QHJ
* @date 2022/7/29 10:08
* @description: 线程池
*/
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建服务,创建线程池(参数为线程池的大小)
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 执行任务
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
executorService.execute(new MyThread());
// 关闭连接
executorService.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + i);
}
}
}
// 通知演员表演
this.notifyAll();
this.voice = voice;
this.flag = !this.flag;
}
}
九、线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似于生活中的公共交通工具。
好处:提高响应速度(减少了创建新线程的时间)、降低资源消耗(重复利用线程池中线程,不需要每次都创建)、便于线程管理。
- 常用参数:
-
corePoolSize:核心线程数
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置 allowCoreThreadTimeout = true 后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过 keepAliveTime 后会被回收。
线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于 corePoolSize,则马上创建新线程并运行这个任务。
-
maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程并执行。
-
keepAliveTime:空闲线程存活时间
当一个可被回收的线程的空闲时间大于 keepAliveTime,就会被回收。
可被回收的线程:设置 allowCoreThreadTimeout = true 的核心线程。
大于核心线程数的线程(非核心线程)。
-
unit:时间单位
keepAliveTime的时间单位:
TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.SECONDS
TimeUnit.MINUTES
TimeUnit.HOURS
TimeUnit.DAYS -
workQueue:工作队列
存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被 execute() 方法提交的 Runnable 任务。工作队列实现了 BlockingQueue 接口。
JDK 默认的工作队列有五种:
- ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
- LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为 Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用 Condition 进行线程间的唤醒和等待。
- SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
- PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
- DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。
-
threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等。
默认线程工厂:
/** * The default thread factory */ static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
-
handler:拒绝策略
当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。
可以自定义拒绝策略,拒绝策略需要实现 RejectedExecutionHandler 接口。
JDK默认的拒绝策略有四种:
- AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常。
- DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- CallerRunsPolicy:由调用线程处理该任务。
默认拒绝策略:
/** * The default rejected execution handler */ private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); public static class AbortPolicy implements RejectedExecutionHandler { /** * Creates an {@code AbortPolicy}. */ public AbortPolicy() { } /** * Always throws RejectedExecutionException. * * @param r the runnable task requested to be executed * @param e the executor attempting to execute this task * @throws RejectedExecutionException always */ public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString()); } }t/Anenan/article/details/115603481
-
使用线程池
JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors。
-
ExecutorService:真正的线程池接口,常见的子类有 ThreadPoolExecutor
- viod execute(Runnable command):执行任务/命令,没有返回值,一般用来执行 Runnable;
- < T >Future< T > submit(Callable< T > task):执行任务,有返回值,一般用来执行Callable;
- void shutdown():关闭连接池。
-
Executors:工具类、线程池的共产类,用于创建并返回不是同类型的线程池。
package cn.qhj.pool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author QHJ * @date 2022/7/29 10:08 * @description: 线程池 */ public class ThreadPoolTest { public static void main(String[] args) { // 创建服务,创建线程池(参数为线程池的大小) ExecutorService executorService = Executors.newFixedThreadPool(10); // 执行任务 executorService.execute(new MyThread()); executorService.execute(new MyThread()); executorService.execute(new MyThread()); executorService.execute(new MyThread()); // 关闭连接 executorService.shutdown(); } } class MyThread implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++){ System.out.println(Thread.currentThread().getName() + i); } } }
-