有经验的佛山网站建设,上海旅游网站建设情况,哪个厂家的广州网站建设,建设360导航网站的目的是什么文章目录 一、认识线程#xff08;Thread#xff09;1.1 概念1.1.1 什么是线程1.1.2 为什么要有线程1.1.3 进程和线程的区别#xff08;重要#xff09;1.1.4 Java的线程和操作系统线程的关系 1.2 第一个多线程 程序1.3 创建线程#xff08;重要#xff09;1.3.1 继承 Tr… 文章目录 一、认识线程Thread1.1 概念1.1.1 什么是线程1.1.2 为什么要有线程1.1.3 进程和线程的区别重要1.1.4 Java的线程和操作系统线程的关系 1.2 第一个多线程 程序1.3 创建线程重要1.3.1 继承 Tread 类1.3.2 实现 Runnable 接口1.3.3 匿名内部类 创建Thread 子类对象1.3.4 匿名内部类 创建实现 Runnable 接口的Thread子类对象1.3.5 lambda 表达式创建实现 Runnable 接口的Thread 的⼦类对象 二、Thread 类及常用方法2.1 Thread 常见的构造方法2.2 Thread 的几个常见属性2.3 启动线程 - start() 面试题2.4 中断一个线程2.5 等待一个线程- join()2.6 获取当前线程引用2.7 休眠当前线程 三、线程的状态3.1 观察线程的所有状态 四、多线程带来的风险-线程安全(重点)4.1 观察线程不安全4.2 什么是线程安全4.3 线程不安全的原因 4.4 解决上述的线程不安全问题五 synchronized 关键字监视器锁 monitor lock5.1 synchronized 的特性5.2 synchronized 的使用5.2.1 修饰代码块 明确指明锁的哪个对象5.2.2 修饰方法 5.3 Java 标准库中的线程安全类 六、volatile 关键字6.1 volatile 保证内存可见性6.2 volatile 不保证原子性 七、wait 和 notify7.1 wait()方法7.2 notify()方法7.3 notifyAll()方法7.4 wait 和 sleep 的对比重要 八、多线程案例8.1 单例模式8.1.1 饿汉模式8.1.2 懒汉模式 8.2 阻塞队列8.2.1 阻塞队列的定义8.2.2 消费者模型8.2.3 标准库中的阻塞队列8.2.4 阻塞队列的模拟实现 8.3 定时器8.3.1 什么是定时器8.3.2 标准库中的定时器8.3.3 模拟实现定时器 8.4 线程池8.4.1 什么是线程8.4.2 标准库中的线程重要8.4.3 模拟实现线程池 九、对比线程和进程9.1 线程的优点9.2 线程和进程的区别 一、认识线程Thread
1.1 概念
1.1.1 什么是线程
⼀个线程就是⼀个执⾏流每个线程之间都可以按照顺序执⾏⾃⼰的代码多个线程之间同时执⾏着多份代码。
1.1.2 为什么要有线程
并发编程成为“刚需”。 单核 CPU 的发展遇到了瓶颈要想提⾼算⼒就需要多核 CPU⽽并发编程能更充分利⽤多核 CPU 资源。有些任务场景需要 “等待 IO”为了让等待 IO 的时间能够去做⼀些其他的⼯作也需要⽤到并发编程。 虽然多进程也能实现 并发编程但是线程⽐进程更轻量。 创建线程比创建进程更块销毁线程比销毁进程更快调度线程比调度进程更快 线程虽然⽐进程轻量但还不满⾜于是⼜有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
1.1.3 进程和线程的区别重要
进程包含线程线程不能独立存在要依附于进程每个进程⾄少有⼀个线程存在即主线程进程和线程 都是用来实现并发编程场景的但线程比进程更轻量更高效进程和进程之间不共享资源同⼀个进程的线程之间共享资源内存和硬盘进程是系统分配资源的最⼩单位线程是系统调度的最⼩单位进程之间是独立的⼀个进程挂了⼀般不会影响到其他进程但⼀个线程挂了很大可能影响同进程内的其他线程(整个进程崩溃) 1.1.4 Java的线程和操作系统线程的关系
线程是操作系统中的概念操作系统内核实现了线程这样的机制并且对⽤户层提供了⼀些 API 供⽤⼾使⽤。 Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装。
1.2 第一个多线程 程序
感受多线程程序和普通程序的区别:
每个线程都是⼀个独⽴的执⾏流多个线程之间 “并发” 执⾏
/*** 通过创建一个 继承 thread类 的类 的方式创建线程重写run方法*/
class MyThread extends Thread{Overridepublic void run() {//这个方法是线程的入口方法while(true){System.out.println(hello thread);//重写父类的 run方法 并没有声明异常子类重写这个方法也不能声明异常只能采用捕获异常的方式try {//设置当前线程暂停执行指定的时间间隔1秒然后再恢复执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo1 {public static void main(String[] args) throws InterruptedException {Thread thread new MyThread();//start 和 run 都是Thread 的成员// run 只描述线程的入口线程要做什么//start 是真正调用了系统API在系统中创建线程让线程再调用 runthread.start();while (true){System.out.println(hello main);// sleep方法可能抛出异常受查异常---显示处理---声明或捕获异常//设置当前线程暂停执行指定的时间间隔1秒然后再恢复执行Thread.sleep(1000);}}
}1.3 创建线程重要
1.3.1 继承 Tread 类
继承 Thread 来创建⼀个线程类重写run方法 具体实现参考上述 1.2.
1.3.2 实现 Runnable 接口
/*** 实现 Runnable接口重写run*/
class MyRunnable implements Runnable{Overridepublic void run() {while(true){System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo2 {public static void main(String[] args) {Runnable runnable new MyRunnable();Thread thread new Thread(runnable);thread.start();while(true){System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}1.3.3 匿名内部类 创建Thread 子类对象 /*** 使用匿名内部类创建 Thread 子类对象*/
public static void main(String[] args) {Thread thread new Thread(){Overridepublic void run() {while(true){System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};thread.start();while(true){System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}1.3.4 匿名内部类 创建实现 Runnable 接口的Thread子类对象
/*** 使用匿名内部类创建 Runnable 的子类对象*/
public static void main(String[] args) {Thread thread new Thread(new Runnable() {Overridepublic void run() {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});thread.start();while (true){System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}1.3.5 lambda 表达式创建实现 Runnable 接口的Thread 的⼦类对象
/*** 使用 lambda 表达式创建 Runnable子类对象*/
public static void main(String[] args) {Thread thread new Thread(() - {while (true) {System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();while (true){System.out.println(hello main);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}二、Thread 类及常用方法
Thread 类是 JVM ⽤来管理线程的⼀个类换句话说每个线程都有⼀个唯⼀的 Thread 对象与之关联。Thread 类的对象就是⽤来描述⼀个线程执⾏流的JVM 会将这些 Thread 对象组织起来⽤于线程调度线程管理。
2.1 Thread 常见的构造方法
方法说明Thread()创建线程对象Thread(Runnable target)使用 Runnable 对象创建线程对象Thread(String name)创建线程对象并命名Thread(Runnable target,String name)使用Runnable 对象创建线程对象并命名Thread(ThreadGroup group,Runnable target(了解))线程可以被用来分组管理分好的组为线程组
Thread t1 new Thread();
Thread t2 new Thread(new MyRunnable());
Thread t3 new Thread(这是新线程的名字);
Thread t4 new Thread(new MyRunnable(), 这是新线程的名字);/*** 给线程起名字 这是新线程*/
public static void main(String[] args) {Thread thread new Thread(() - {while (true){System.out.println(hello thread);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}},这是新的线程);// 创建线程thread.start();}2.2 Thread 的几个常见属性 ID 是线程的身份标识不同的线程不会重复。(id 是 Java 给这个线程分配的不是系统API提供的线程 id也不是PCB中的 id)名称是线程的名字明确知道是哪一个线程状态描述线程当前所处状态是就绪状态还是运行状态又或者是阻塞状态等优先级影响系统在微观上进行的调度 图中的方法提供API可以设置/获取优先级但在应用程序的角度很难察觉出优先级带来的差异后台线程守护线程不结束并不影响整个进程的结束前台线程一个Java进程中如果前台线程没有结束整个进程一定不会结束。默认情况下一个线程是前台线程。 是否存活Thread 对象的生命周期比系统内核中的线程更长一些就会导致Thread 对象还存在内核中的线程已经销毁了的情况使用 isAliva 判定内核线程是否已经销毁 线程中断参考下文
2.3 启动线程 - start() 面试题
之前我们已经看到了 通过重写 run ⽅法创建⼀个线程对象但线程对象被创建出来并不意味着线程就开始运⾏。 重写 run ⽅法是描述线程要做的事情调⽤ start() ⽅法线程才真的在操作系统的底层创建出⼀个线程 start 和 run 的区别
strat 方法内部会调用系统API在系统内核中创建线程run 方法只是单纯的描述该线程要执行的内容会在start 创建好线程后自动被调用
start 和 run 方法的本质区别就是 start 会在系统内部创建出新线程而 run 不会。
2.4 中断一个线程
中断一个线程其实就是终止或打断线程意思就是让一个线程停止运行销毁。在Java中要销毁或者说终止线程做法比较唯一就是让 run 方法尽快执行结束。 常见方式
通过共享标记进行沟通
public class demo8 {//自定义变量作为标志位private static boolean isQuit false;public static void main(String[] args) throws InterruptedException {Thread thread new Thread(() - {//while 尽快结束就意味着 run 方法尽快结束while (!isQuit){//线程的实际工作内容System.out.println(线程工作中);try {//新线程休眠暂停时间毫秒Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(线程工作结束);});//创建线程thread.start();//主线程休眠暂停时间毫秒Thread.sleep(5000);//设置线程要结束了isQuit true;System.out.println(设置标志位 isQuit 为 true);}
}2. 调用 Thread 内部提供的 interrupt 或 isInterrupted 方法 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记。
方法说明public void interrupt()终止线程将线程的终止标志设置为 true。如果线程正在阻塞sleep、wait、join等调用 interrupt 终止将抛异常否则只是设置终止标志不会终止线程执行public static boolean interrupted()静态方法判断当前线程是否已被终止并清除终止状态多次调用只有第一次返回 true如果线程终止返回 true否则返回 falsepublic boolean isInterrupted()判断线程是否已被终止但不清除终止状态如果线程终止返回 true否则返回 false public class demo9 {public static void main(String[] args) throws InterruptedException {Thread thread new Thread(() -{//判断线程是否是终止状态while(!Thread.currentThread().isInterrupted()){System.out.println(线程工作中);try {//新线程休眠时间Thread.sleep(1000);} catch (InterruptedException e) {//抛出异常循环继续进行假装没听到e.printStackTrace();// 1.可以在结束前做一些其他工作完成后再结束// 将其他工作的代码放在这里System.out.println(做一些其他工作);// 2. 使用 break 手动结束循环即结束线程break;}}});//创建线程thread.start();//主线程休眠时间Thread.sleep(5000);System.out.println(线程 thread 该终止了);thread.interrupt();}
}注意
如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起则以 InterruptedException 异常的形式通知清除终止标志 当出现 InterruptedException 的时候要不要结束线程取决于 catch 中代码的写法可以选择忽略这个异常也可以跳出循环结束线程 否则只是内部的⼀个中断标志被设置thread 可以通过 Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置不清除中断标志这种⽅式通知收到的更及时即使线程正在 sleep 也可以⻢上收到
2.5 等待一个线程- join()
有时我们需要等待⼀个线程完成它的⼯作后才能进⾏⾃⼰的下⼀步⼯作也即是说让一个线程等待另一个线程执行结束再继续执行本质上就是在控制线程结束的顺序。
方法说明public void join()等待线程结束public void join(long millis)等待线程结束 最多等待 millis 毫秒public void join(long millis, int nanos)同理但可以更高精度
public class demo10 {public static void main(String[] args) throws InterruptedException {Thread thread new Thread(() - {for (int i 0; i 5; i) {System.out.println(线程在工作);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});//创建线程thread.start();System.out.println(等待开始);thread.join();System.out.println(等待结束);}
}2.6 获取当前线程引用
方法说明public static Thread currentThread()返回当前线程对象引用
public class demo11 {public static void main(String[] args) {Thread thread Thread.currentThread();System.out.println(thread.getName());}
}2.7 休眠当前线程
线程的调度是不可控的这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。
方法说明public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒public static void sleep(long millis,int nanos) throws InterruptedException更高精度休眠当前线程 millis 毫秒
public static void main2(String[] args) throws InterruptedException {long start System.currentTimeMillis();Thread.sleep(3000);long end System.currentTimeMillis();// 每次进程休眠时间不确定但是一个大于等于3000的数例如 3014System.out.println(start - end (end-start));
}三、线程的状态
3.1 观察线程的所有状态
线程的状态是一个枚举类型Thread.State
public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}
}NEW安排了工作确定了线程工作的内容/已经重写了run 方法还没有开始执行RUNNABLE线程是可以执行的换句话说线程是正在执行或已经准备就绪时刻可以开始执行TERMINATED终结的意思Thread 对象还在但内核中的线程已经销毁了或者说线程已经执行完了TIMED_WAITING阻塞由于 sleep 固定时间设置休眠时间的方式产生的阻塞WAITING阻塞由于 weit 不固定时间不确定到底要等待多长时间的方式产生的阻塞BLOCKED阻塞由于锁竞争而产生的阻塞
public static void main(String[] args) throws InterruptedException {Thread thread new Thread(()-{});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}public static void main(String[] args) throws InterruptedException {Thread thread new Thread(()-{while (true){}});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();for (int i 0; i 5; i) {// 获取 创建线程后的状态--RUNNABLESystem.out.println(thread.getState());Thread.sleep(1000);}// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}public static void main(String[] args) throws InterruptedException {Thread thread new Thread(()-{while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();for (int i 0; i 5; i) {// 获取 创建线程后的状态--RUNNABLESystem.out.println(thread.getState());Thread.sleep(1000);}// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}四、多线程带来的风险-线程安全(重点)
4.1 观察线程不安全
private static int count 0;
// 使用两个线程实现 count 在每个线程自增 5w ---多个线程修改同一个变量
public static void main(String[] args) throws InterruptedException {Thread thread1 new Thread(()-{// count 自增 5w 次for (int i 0; i 50000; i) {count;}});Thread thread2 new Thread(()-{// count 自增 5w 次for (int i 0; i 50000; i) {count;}});//两个线程同时执行thread1.start();thread2.start();//等待两个线程都结束再打印 count 的值thread1.join();thread2.join();//预期的 count 是10wSystem.out.println(count: count);
}4.2 什么是线程安全
在多线程环境下代码的运行结果和在单线程环境下运行的结果相同就说这个多线程程序是安全的。
4.3 线程不安全的原因 操作系统中线程的调度是随机的是在系统内核中实现的我们无法改变但是我们必须要保证在任何执行顺序下代码都能正常工作 两个线程对同一个变量 进行修改。一个线程修改一个变量、两个线程修改不同变量 或者 两个线程对同一个变量读取都不会有安全问题。 修改操作不是原子的 内存可见性问题 指令重排序问题
4.4 解决上述的线程不安全问题 public class demo14 {private static int count 0;public static void main(String[] args) throws InterruptedException {Object lock new Object();Thread thread1 new Thread(()-{// count 自增 5w 次for (int i 0; i 50000; i) {//加锁synchronized (lock){count;}}});Thread thread2 new Thread(()-{// count 自增 5w 次for (int i 0; i 50000; i) {//加锁synchronized (lock){count;}}});thread1.start();thread2.start();//等待两个线程都结束再打印 count 的值thread1.join();thread2.join();//预期的 count 是10wSystem.out.println(count: count);}
}五 synchronized 关键字监视器锁 monitor lock
5.1 synchronized 的特性
互斥 synchronized 会起到互斥效果某个线程执行到某个对象的 synchronized 中时其他线程 执行了同一个对象的synchronized 就会阻塞等待 进入 synchronized 修饰的代码块就是加锁出 synchronized 修饰的代码块就是 解锁 synchronised 底层是用操作系统的 mutex lock 来实现 可重入 一个线程连续对 一把锁 / 同一个锁对象 加锁两次不会出现死锁的情况就是可重入锁
5.2 synchronized 的使用
5.2.1 修饰代码块 明确指明锁的哪个对象
锁任意对象
public static void main(String[] args) {Object lock new Object();Thread thread1 new Thread(()-{synchronized (lock){}});
}锁当前对象
class SynchronizedDemo{public void method(){synchronized (this){}}
}5.2.2 修饰方法
修饰普通方法实例方法
class SynchronizedDemo{int count;public void method(){synchronized (this){count;}}synchronized public void method2(){count;}
}修饰静态方法相当于对类对象加锁
class SynchronizedDemo{int count;public void method(){synchronized (this){count;}}synchronized public void method2(){count;}synchronized public static void method3(){}
}使用实例
public static void main(String[] args) throws InterruptedException {SynchronizedDemo synchronizedDemo new SynchronizedDemo();Thread thread1 new Thread(()-{for (int i 0; i 50000; i) {//synchronizedDemo.method();synchronizedDemo.method2();}});Thread thread2 new Thread(()-{for (int i 0; i 50000; i) {//synchronizedDemo.method();synchronizedDemo.method2();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(synchronizedDemo.count);//100000
}5.3 Java 标准库中的线程安全类
Java 标准库中有很多都是线程不安全的这些类可能会涉及多线程修改共享数据又没有加锁措施
ArrayListLinkedListHashMapTreeMapHashSetTreeSetString Builder
也有一些线程安全的类使用一些锁机制来控制
Vector(不推荐使用)HashTableConcurrentHashMapStringBuffer
还有的虽然没有加锁但不涉及修改也是线程安全的
String
六、volatile 关键字
6.1 volatile 保证内存可见性
写代码实现用户输入线程结束条件isQuit 0线程可以立刻执行结束
public static int isQuit 0;
public static void main(String[] args) {Thread thread new Thread(()-{while (isQuit 0){//循环体里什么都没干一秒会执行很多次}System.out.println(线程 thread 结束);});thread.start();Thread thread1 new Thread(()-{System.out.println(输入 isQuit);Scanner scanner new Scanner(System.in);//一旦用户输入值不是0这时线程thread 执行结束isQuit scanner.nextInt();});thread1.start();
}运行后发现 修改后的代码
public class demo17 {public static volatile int isQuit 0;public static void main(String[] args) {Thread thread new Thread(()-{while (isQuit 0){//循环体里什么都没干一秒会执行很多次}System.out.println(线程 thread 结束);});thread.start();Thread thread1 new Thread(()-{System.out.println(输入 isQuit);Scanner scanner new Scanner(System.in);//一旦用户输入值不是0这时线程thread 执行结束isQuit scanner.nextInt();});thread1.start();}
}6.2 volatile 不保证原子性
volatile 和 synchronized 有着本质的区别。synchronized 保证原⼦性, volatile 保证内存可⻅性。 示例 多线程实现计数器 count
class Count{private static int count 0;//自增成为原子性操作synchronized void increase(){count;}public int getCount(){return count;}
}
public class demo18 {public static void main(String[] args) throws InterruptedException {Count count new Count();Object lock new Object();Thread thread1 new Thread(() -{for (int i 0; i 50000; i) {count.increase();}});Thread thread2 new Thread(() -{for (int i 0; i 50000; i) {count.increase();}});thread1.start();thread2.start();//两个线程都结束在继续执行主线程thread1.join();thread2.join();//预期结果 10wSystem.out.println(count.getCount());//10w}
}现在去掉修饰 increase 方法的 synchronized加锁给 count 加 volatile 关键字进行修饰
class Count{private static volatile int count 0;void increase(){count;}public int getCount(){return count;}
}
public class demo18 {public static void main(String[] args) throws InterruptedException {Count count new Count();Object lock new Object();Thread thread1 new Thread(() -{for (int i 0; i 50000; i) {count.increase();}});Thread thread2 new Thread(() -{for (int i 0; i 50000; i) {count.increase();}});thread1.start();thread2.start();//两个线程都结束在继续执行主线程thread1.join();thread2.join();//预期结果 10wSystem.out.println(count.getCount());//结果却是一个不大于10w 的数}
}上述代码运行结果证明 volatile 关键字并不能保证原子性
七、wait 和 notify 线程之间是抢占式执行所以线程之间的执行先后顺序我们并不知道但实际开发中有时候希望合理的协调多个线程之间的执行先后顺序就像 打一场篮球比赛 球场上的每个运动员都是一个独立的线程而要完成进攻得分需要多个运动员相互配合按照一定的顺序执行一定的动作即可认为有的线程要传球有的线程要进球这样的动作 而要完成协调工作就会涉及三个方法
wait() / wait(long timeout)使当前线程进入等待状态notify / notifyAll()唤醒当前对象上等待的线程
需要注意的是wait、notify、notifyAll 都是 Object 类的方法
7.1 wait()方法 wait方法执行时做的事
释放当前的锁线程进入阻塞状态当线程被唤醒时重新获取这个锁
使用 wait 要搭配 synchronized 确保在 wait 前获取到锁脱离 synchronized 使用 wait 会抛出异常。
public static void main(String[] args) throws InterruptedException {Object object new Object();synchronized (object){System.out.println(wait 等待前);// wait 放在 synchronized 来保证获取到锁object.wait();System.out.println(wait 等待后);}}调用 wait 不一定就只有一个线程调用N个线程都可以调用 wait 这N线程都调用后都处于阻塞状态。 wait 结束等待的条件
其他线程调用该对象的 notify方法唤醒时会有一个重新获取锁的过程wait等待时间超时在调用wait 方法时就指定等待时间其他线程调用该等待线程的interrupted 方法使wait抛出 InterruptedException 异常
7.2 notify()方法
notify()方法是唤醒等待的线程
notify()方法也要和 synchronized 搭配使用因为在唤醒等待的线程的时候要重新获取这个锁否则也会抛出异常如果有多个线程等待线程程调度器就会随机挑选一个等待的线程notify()方法后当前线程不会马上释放这个锁要等到执行notify()方法的线程执行完后才会释放这个锁
代码实现创建两个线程都会等待第三个线程创建后手动确定唤醒哪一个
public class demo21 {public static void main(String[] args) {Object object2 new Object();Thread thread1 new Thread(() -{synchronized (object2){System.out.println(线程 thread1 等待前);try {object2.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(线程 thread1 等待后);}});Thread thread2 new Thread(() -{synchronized (object2){System.out.println(线程 thread2 等待前);try {object2.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(线程 thread2 等待后);}});Thread thread3 new Thread(() -{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object2){System.out.println(线程唤醒);object2.notify();System.out.println(线程已唤醒);}System.out.println(调用notify 的线程执行完);});thread1.start();thread2.start();thread3.start();}
}7.3 notifyAll()方法
notify⽅法只是唤醒某⼀个等待线程使⽤notifyAll⽅法可以⼀次唤醒所有等待的线程。
public class demo22 {public static void main(String[] args) {Object object1 new Object();Thread thread1 new Thread(() -{synchronized (object1){System.out.println(线程 thread1 等待前);try {object1.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(线程 thread1 等待后);}});Thread thread2 new Thread(() -{synchronized (object1){System.out.println(线程 thread2 等待前);try {object1.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println(线程 thread2 等待后);}});Thread thread3 new Thread(() -{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object1){System.out.println(线程唤醒);object1.notifyAll();System.out.println(线程唤醒后);}System.out.println(调用 notifyAll 的线程执行完);});thread1.start();thread2.start();thread3.start();}
}注意虽然是同时唤醒使用同一个锁的 所有的线程但唤醒的所有线程需要竞争锁并不是同时执行仍有先后执行。
7.4 wait 和 sleep 的对比重要
起源上wait 是 Object 的普通方法sleep 是 Thread 的静态方法应用场景上wait 用于实现线程间的协调需要搭配监视器synchronized使用而 sleep 用于让线程休眠一段时间不需要搭配监视器锁的释放上在调用 wait 时会释放对象锁其他线程可以获取该锁而调用 sleep 时不会释放对象锁其他线程不能获取该锁唤醒方式上wait 通过其他线程调用相同锁对象的 notify 或 notifyAll 来唤醒而 sleep 在指定休眠时间过后自动唤醒或者通过其他线程中断它来提前唤醒
八、多线程案例
8.1 单例模式
单例模式是校招最常考的设计模式之一另一个是工厂模式。 设计模式就好比象棋中的棋谱红⽅当头炮⿊⽅⻢来跳针对红⽅的⼀些⾛法⿊⽅应招的时候有⼀些固定的套路。按照套路来⾛局势就不会吃亏。 开发过程中针对特定的问题场景大佬总结出固定的套路按固定套路来实现代码不会吃亏。 单例模式一些场景中要求某个类只有一个实例对象不会再创建出多个实例。 单例模式实现方式有很多最常见有 “饿汉” 和 “懒汉” 两种
8.1.1 饿汉模式
类加载时就创建实例。
class Singleton{//类加载时就创建实例private static Singleton instance new Singleton();//保证没有其他的构造方法再创建实例private Singleton(){ };//只获取实例public static Singleton getInstance(){return instance;}
}8.1.2 懒汉模式
类加载时不创建实例第⼀次使⽤时才创建实例。
单线程版
class SingletonLazy{private static SingletonLazy instance null;private SingletonLazy(){ };//在第一次使用时创建实例public static SingletonLazy getInstance(){if(instance null){instance new SingletonLazy();}return instance;}
}多线程版 多线程版的就不安全了线程安全问题发生在首次创建实例的时候如果多个线程中同时调用 getInstance 方法就可能创建出多个实例。多线程可能既会获取又会修改 Instance 使用 synronized 对 创建实例的方法加锁。
class SingletonLazy{private static SingletonLazy instance null;public synchronized static SingletonLazy getInstance(){if(instance null){instance new SingletonLazy();}return instance;}
}也可以写成下面的代码是同样效果
class SingletonLazy{private static SingletonLazy instance null;public static SingletonLazy getInstance(){synchronized (SingletonLazy.class){if(instance null){instance new SingletonLazy();}}return instance;}
}多线程版改进 一旦以上述代码形式执行多线程每一次调用 getInstance 都会先加锁加锁开销很大一旦加锁就很可能会引发锁冲突进而会引起阻塞锁竞争的频率就会很高但是实际上发生线程安全问题只是在最开始对象还没有new 的时候对象被 new 过后就不需要再修改只有读操作 那么是否有办法让代码既线程安全又不会对执行效率有太多影响呢? 在加锁的外层进行判断是否需要加锁如果已经有对象了线程就安全了不需要加锁如果没有对象就会有线程安全问题需要加锁
class SingletonLazy{private static volatile SingletonLazy instance null;public static SingletonLazy getInstance(){//判断是否需要加锁if(instance null){synchronized(SingletonLazy.class){//判断是否需要new 对象if(instance null){instance new SingletonLazy();}}}return instance;}
}指令重排序 编译器进行的优化—在不改变逻辑的前提下调整代码执行顺序来提高执行效率可能会对上述代码产生影响。 对于指令重排序问题解决办法是 使用 volatile 关键字修饰 instance 保证编译器不进行优化也就不会出现指令重排序的问题。
class SingletonLazy{private static volatile SingletonLazy instance null;public static SingletonLazy getInstance(){//判断是否需要加锁if(instance null){synchronized(SingletonLazy.class){//判断是否需要new 对象if(instance null){instance new SingletonLazy();}}}return instance;}
}8.2 阻塞队列
8.2.1 阻塞队列的定义
阻塞队列是一种特殊的队列也遵守先进先出的原则。 阻塞队列是一种线程安全的数据结构有下面两个特性
当队列元素满的时候继续入队列就会阻塞一直到其他线程从队列中取走元素当队列为空的时候继续出列也会阻塞一直到其他线程向队列中插入元素
8.2.2 消费者模型
阻塞队列的经典应用场景就是 “生产者消费者模型”一种典型的开发模式。
8.2.3 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列如果需要使⽤阻塞队列直接使⽤标准库中的即可。
BlockingQueue 是⼀个接口继承自 Queue实现的方法有两种基于数组和基于链表实现的类是 ArrayBlockingQueue 和 LinkedBlockingQueueput ⽅法⽤于阻塞式的⼊队列take 方法⽤于阻塞式的出队列BlockingQueue 也有 offerpollpeek 等⽅法但这些⽅法不具有阻塞特性不建议使用
//基于数组的实现 需要指定容量否则会报错
BlockingQueueString blockingQueue new ArrayBlockingQueue(1000);
//基于链表的实现
BlockingQueueString queue new LinkedBlockingQueue();
// ⼊队列如果队列满就会阻塞直到不再满
queue.put(abc);
// 出队列 如果队列为空就会阻塞直到不再为空.
String elem queue.take();8.2.4 阻塞队列的模拟实现
通过 “循环队列” 的⽅式来实现使⽤ synchronized 进⾏加锁控制使用 volatile 防止内存可见性问题代码中涉及共享数据的修改时编译器可能会优化put 插⼊元素的时候判定如果队列满了就进⾏ wait。 注意要在循环中进⾏ wait被唤醒有可能是因为使用 intrruput 终止线程时唤醒 wait抛出异常线程正常结束但如果是捕获了异常代码会向后走但是不知道此时队列是否已满还要进行判断时可能队列也是满了。使用 wait 往往使用 while 作为条件判断方式目的在于 让 wait 被唤醒后还能再确认一次是否仍满足条件。take 取出元素的时候判定如果队列为空就进⾏ wait (也是循环 wait) 。
class MyBlockQueue{//队列存储的数据最大长度可以直接指定也可以使用构造方法自定义指定private String[] elem new String[1000];//队列的首位置private volatile int head;//队列的结束位置的下一位private volatile int rear;//记录队列元素个数private volatile int size;// 锁对象private Object locker new Object();//入队public void push(String s) throws InterruptedException {// 由于方法中有很多数据可能会修改可能会引起内存可见性问题// 而又要尽量减少锁的使用加锁开销会很大所以对整体加一个锁synchronized (locker){while (size elem.length){//队列已满//进入阻塞状态locker.wait();//再次唤醒 wait 的时候还要判断队列是否满}elem[rear] s;rear;if(rear elem.length){rear 0;}size;//唤醒的是 take方法中的 wait由于空队引起的阻塞locker.notify();}}//出队public String take() throws InterruptedException {// 由于方法中有很多数据可能会修改可能会引起内存可见性问题// 而又要尽量减少锁的使用加锁开销会很大所以对整体加一个锁synchronized (locker){while (size 0){//空队列//进入阻塞等待locker.wait();//再次唤醒 wait 的时候还要判断队列是否满}String ret elem[head];head;if(head elem.length){head 0;}size--;//唤醒的是 push 方法 中的 wait由于队满而引起的阻塞locker.notify();return ret;}}
}生产者消费者模型
public static void main(String[] args) {BlockingQueueString blockingQueue new ArrayBlockingQueue(1000);Thread threadProduct new Thread(() -{int num 1;while (true){try {blockingQueue.put(num);System.out.println(生产元素num);num;//生产元素慢 0.5 秒Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}},生产者);Thread threadCustomer new Thread(() -{while (true){try {String date blockingQueue.take();System.out.println(消费元素date);} catch (InterruptedException e) {e.printStackTrace();}}},消费者);threadCustomer.start();threadProduct.start();
}8.3 定时器
8.3.1 什么是定时器
定时器是软件开发的一个重要组件类似于闹钟作用是设定一个时间当达到这个时间后就执行一个指定好的代码。 定时器作为实际开发中常用的组件比如在网络通信中如果对方在500毫秒内没有返回数据就会断开连接尝试重新连接。
8.3.2 标准库中的定时器
Java标准库中提供一个 Timer 类就是定时器的实现Timer 类的核心方法是 schedule 翻译成中文 有安排的意思。
schedule 包含两个参数第一个参数是将要执行的任务代码第二个参数是指定等待多长时间才执行单位毫秒。
public static void main(String[] args) {Timer timer new Timer();//给定时器安排一个任务---》预定在一个3秒后执行起始时间是从schedule开始计算timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(3000);}},3000);//时间是毫秒级别timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(1000);}},1000);timer.schedule(new TimerTask() {Overridepublic void run() {System.out.println(2000);}},2000);System.out.println(程序开始);
}8.3.3 模拟实现定时器
定时器构成
有一个类用来描述任务任务内容和执行时间有一个优先级队列存放所有的任务队首元素就是最先要执行的任务有一个扫描线程判断任务是否到了要执行的时间
//定义一个类用来描述任务包含任务内容和执行时间
//任务要放入优先级队列必须是可比较的要实现比较的接口重写方法
class MyTimeTask implements ComparableMyTimeTask{private Runnable runnable;private long time;Overridepublic int compareTo(MyTimeTask o) {//创建的优先级队列中时间最小的放队首--先执行return (int) (this.time - o.time);}public MyTimeTask(Runnable runnable, long time) {this.runnable runnable;//保存绝对时间记录到什么时间才开始执行任务this.time System.currentTimeMillis() time;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}class MyTimer{//存储要执行的任务private PriorityQueueMyTimeTask priorityQueue new PriorityQueue();//锁对象private Object locker new Object();//安排任务public void schedule( Runnable runnable,long time){synchronized (locker){priorityQueue.offer(new MyTimeTask(runnable, time));//唤醒等待的线程locker.notify();}}//创一个扫描线程public MyTimer(){Thread thread new Thread(() -{//一直扫描队首的任务查看是否达到执行的时间while (true){try {synchronized (locker){while (priorityQueue.isEmpty()){//空的任务队列》等待直到队列不为空才被唤醒locker.wait();}MyTimeTask myTimeTask priorityQueue.peek();//获取当前时间long curTime System.currentTimeMillis();if (curTime myTimeTask.getTime()){//任务时间已经达到--执行任务myTimeTask.getRunnable().run();//从任务队列中删除priorityQueue.poll();}else {// 没有达到任务时间不执行任务等到任务要开始执行// wait 方法使线程阻塞线程不会在cpu上调度不占cpu资源// 避免忙等什么都不干也没有休息一直占用cpu资源locker.wait(myTimeTask.getTime() - curTime);}}}catch (InterruptedException e) {e.printStackTrace();}}});thread.start();}
}public class demo26 {public static void main(String[] args) {MyTimer myTimer new MyTimer();myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(3000);}},3000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(1000);}},1000);myTimer.schedule(new Runnable() {Overridepublic void run() {System.out.println(2000);}},2000);System.out.println(程序开始);System.out.println(计数器开始启动);}
}8.4 线程池
8.4.1 什么是线程
线程诞生是因为进程的创建和销毁太重量开销比较大效率就比较慢而当线程的创建和销毁也频繁的时候那么线程的开销也不能忽视为了提高效率Java 中有了线程池这个概念用来减少创建和销毁线程的开销当在创建第一个线程的时候就把要使用的其他线程也提前创建好放在池子里后续使用的时候直接从池子里取出来。
8.4.2 标准库中的线程重要
使用 Executors.newFixedThreadPool(10); 创建出固定线程数量这里是10个的线程池返回值是 ExecutorService 类型通过 ExecutorService.submit 方法将一个任务提交到线程池中
ExecutorService service Executors.newFixedThreadPool(10);
service.submit(new Runnable() {Overridepublic void run() {System.out.println(工程模式创建线程池);}
});Executors 创建线程池的几种方式
newFixedThreadPool创建固定数量的线程池newCachedThreadPool创建线程数目动态增长线程根据需要自动被动的被创建出来的线程池newSingleThreadExecutor创建单个线程的线程池newScheduledThreadPool设置多长时间后执行命令相当于定时器的进阶版不是一个线程负责执行任务而是有多个线程执行到时间的任务
Executors 本质上是 ThreadPoolExecutor 类的封装ThreadPoolExecutor 类核心方法只有两个构造和添加任务submit
8.4.3 模拟实现线程池
方法 submit 将任务加入线程池中使用 一个阻塞队列BlockingQueue组织所有的任务指定线程池中线程的最大数目当线程超过这个最大数目不再创建线程
class MyThreadPool{// 阻塞队列---》 组织/存放 任务private BlockingQueueRunnable blockingQueue new ArrayBlockingQueue(10);// 通过这个方法将任务加入到队列中public void submit(Runnable runnable) throws InterruptedException {// 任务满了 就会阻塞等待blockingQueue.put(runnable);}//创建线程池时创建好线程并执行任务public MyThreadPool(int n){// 创建 n 个线程for (int i 0; i n; i) {// 描述 线程执行的任务Thread thread new Thread(() -{try {// 获取并执行 任务Runnable runnable blockingQueue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}}
}public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool new MyThreadPool(3);for (int i 0; i 10; i) {int count i;myThreadPool.submit(new Runnable() {Overridepublic void run() {// System.out.println(任务i);System.out.println(人任务count);}});}
}九、对比线程和进程
9.1 线程的优点
线程比进程更轻量创建一个线程的开销比创建一个进程的开销小操作系统调度线程比调度进程的效率更高线程占用的资源比进程更少充分利用多处理器cpu可并行的数量
9.2 线程和进程的区别
进程包含线程线程不能独立存在要依附于进程每个进程⾄少有⼀个线程存在即主线程进程和线程 都是用来实现并发编程场景的但线程比进程更轻量更高效进程和进程之间不共享资源同⼀个进程的线程之间共享资源内存和硬盘进程是系统分配资源的最⼩单位线程是系统调度的最⼩单位进程之间是独立的⼀个进程挂了⼀般不会影响到其他进程但⼀个线程挂了很大可能影响同进程内的其他线程(整个进程崩溃)