一,认识synchronized关键字(监视器锁 monitor lock)
1.1synchronized的特性
a)互斥
当某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同一个对象synchronized就会阻塞等待 (进入synchronized修饰的代码块就是加锁 ,离开代码块就是解锁)
阻塞等待:
针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.但需注意上一线程解锁后下一线程不是立刻就能获取锁,线程间不遵守先来后到,而是要靠操作系统来调度。
b)可重入
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息.
1.如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增.
2.解锁的时候计数器递减为0 的时候,才真正释放锁.(才能被别的线程获取到)
1.2 synchronized使用
synchronized修饰普通方法,相当于给this加锁
synchronized修饰静态方法,相当于给类对象加锁
a)修饰代码块:明确指定锁那个对象
java
锁任意对象
public class Synchronized{
private Object locker=new Object();
public void method(){
synchronized(locker){
}
}
}
锁当前对象
public class Synchronized{
public void method(){
synchronized(this){
}
}
}
b)直接修饰普通方法:锁的Synchronized对象
java
public class Synchronized{
public synchronized void method(){
}
}
c)修饰静态方法:锁的Synchronized类的对象
java
public class Synchronized{
public synchronized static void method () {
}
}
二,认识volatile关键字
引入volatile关键字来解决内存可见性问题,但不能保证原子性
解决内存可见性问题:被 volatile 修饰的变量,线程每次读取时,都会直接从主内存读取最新值,不会使用工作内存的缓存副本;每次修改后,会立即同步写回主内存,其他线程能马上看到最新值。加上volatile强制读写内存,速度虽然变慢了但是数据准确性更高
java
class Counter{
public volatile int flag=0;
//此处加入volatile关键字
}
public static void main(String[] args){
Counter counter=new Counter();
Thread t1=new Thread(()->{
while(counter.flag==0){
}
System.out.println("循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数");
counter.flag=scanner.nextInt();
});
t1.start();
t2.start();
}
在上述代码中如不加入volatile关键字t1线程循环就不会结束,因为t1线程读的是自己工作内存中的内容,当t2对flag变量进行修改,此时t1感知不到flag的变化。但加入volatile后,t1线程循环能立即结束。
三,认识wait和notify
wait 和 notify是Java 中用于协调多线程执行顺序的核心方法。由于操作系统无法保证线程的调度顺序,我们可以让需要后执行的线程调用wait
()进入等待状态 ,待先执行的线程完成关键逻辑后,再调用 notify()或 notifyAll()唤醒等待的线程,从而实现线程间的协作与同步wait ()和 notify
()必须在synchronized 同步块 / 方法中调用,且调用对象必须是同一个锁对象,否则会抛出IllegalMonitorStateException。且务必确保先wait后notify,如果有多个线程在同一对象上wait,进行notify时会随机唤醒其中一个线程,一次notify唤醒一个线程,而notifyAll一次唤醒所有线程。
3.1 wait( )方法
wait方法所作事情:1.使当前执行代码的线程进行等待
2.释放当前的锁
3.满足一定条件时被唤醒,重新尝试获取这个锁
wait结束的条件:1.其他线程调用该对象的notify()方法
2.wait等待时间超过wait方法所提供的带有timeout参数版本
3.其他线程调用该等待线程的interrupted方法,导致wait抛出异常
代码示例:
java
public static void main(String[ ] args) throw InterruptedException{
Object object=new Object();
synchronized(object){
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
代码在执行到object.wait()后就会一直进行等待,这时就需要使用notify()来唤醒。
3.1.1wait和sleep对比(本质上没有直接可比性)
**wait():****Object类的成员方法,用于线程间通信与协作,**让当前线程主动释放锁并进入等待状态,直到被其他线程唤醒或超时。
**sleep():****Thread类的静态方法,用于让线程暂停执行,**让当前线程休眠指定时间,时间结束后自动恢复执行。
|------|------------------------------|-----------------------------|
| | wait() | sleep() |
| 使用前提 | 必须在synchronized块中调用,且锁对象一致 | 无限制 |
| 唤醒方法 | 被其他线程通过notify()唤醒(或设置超时自动唤醒) | 休眠时间结束后自动恢复,或被interrupt()中断 |
| 锁行为 | 调用时会主动释放当前所持有的锁 | 调用时不会释放任何锁 |
3.2 notify( )方法
notify方法是唤醒等待的线程:
1.方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,并使他们重新获得该对象的对象锁
2.如果有多个线程等待,则随机挑选出一个为wait状态的线程
3.在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完后才会释放对象锁
代码示例:
java
//等待线程类wait
static class Wait implements Runnable {
private Object locker;
// 构造方法:传入统一的锁对象
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
// 加锁:必须和 notify 使用同一个锁
synchronized (locker) {
while (true) { // 死循环,会一直等待、被唤醒、再等待
try {
System.out.println("wait 开始");
// 1. 调用 wait():**释放锁**,线程进入等待池阻塞
locker.wait();
// 2. 被 notify 唤醒后,重新竞争锁,拿到锁才会走到这里
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
//唤醒线程类notify
static class Notify implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
// 同样使用同一把锁
synchronized (locker) {
System.out.println("notify 开始");
// 唤醒该锁上**一个**等待中的线程
locker.notify();
System.out.println("notify 结束");
}
// 走出同步块,**锁才会真正释放**
}
}
//主线程
public static void main(String[] args) throws InterruptedException {
// 唯一的锁对象,两个线程共用
Object locker = new Object();
Thread t1 = new Thread(new Wait(locker));
Thread t2 = new Thread(new Notify(locker));
t1.start(); // 先启动等待线程
Thread.sleep(1000);// 主线程休眠1秒,保证 t1 先执行并进入 wait
t2.start(); // 再启动唤醒线程
}
3.3 notifyAll( )方法
只需将上面示例代码中唤醒**线程类中notify()修改为notifyAll()**即可;注意:虽然可同时唤醒所有线程,但这些线程需要竞争锁,所以并不是同时执行,而是有先后顺序执行的
四,多线程案例
4.1 单例模式(是一种设计模式,单个实例)
4.1.1 饿汉模式(类加载的同时,创建实例)
java
class Singleton {
// 1. 静态私有实例对象
private static Singleton instance = new Singleton();
// 2. 私有构造方法,禁止外部new
private Singleton() {}
// 3. 公共静态获取实例方法,后续通过此方法获取这里的实例
public static Singleton getInstance() {
return instance;
}
}
4.1.2 懒汉模式(第一次使用时,创建实例)
a)基础版
java
class Singleton {
// 静态实例,初始为 null,先不创建对象
private static Singleton instance = null;
// 私有构造方法:禁止外部 new
private Singleton() {}
// 对外获取实例的方法
public static Singleton getInstance() {
// 判断:如果还没创建实例,就 new 对象
if (instance == null) {
instance = new Singleton();
}
// 直接返回唯一实例
return instance;
}
}
b)pro版: 基础版的实现是线程不安全的
线程线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例. 一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)
++加上 synchronized 可以改善这里的线程安全问题.++
java
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
c)pro max版:在加锁基础上,还可进一步改进
++使用双重 if 判定, 降低锁竞争的频率; 给 instance 加上volatile++
java
class Singleton {
// volatile 关键字,重点
private static volatile Singleton instance = null;
// 私有构造:禁止外部 new
private Singleton() {}
public static Singleton getInstance() {
// 第一层判断
if (instance == null) {
// 类对象锁。synchronized (Singleton.class) 就是用 Singleton 类的 Class 对象作为锁,实现全局互斥,保证在多线程环境下,只有一个线程能执行创建实例的代码,从而保证单例的线程安全。
synchronized (Singleton.class) {
// 第二层判断
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
理解双重 if 判定 / volatile:
加锁 / 解锁是⼀件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了. 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了;同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile . 当多线程首次调用 getInstance, 可能都发现 instance 为 null, 于是又继续往下执行来竞争锁, 其中竞争成功的线程, 再完成创建实例的操作. 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
完整执行流程:1.首次调用:
instance = null,进入第一层判断。2.线程争抢
Singleton.class锁,抢到锁的线程进入同步块。3.同步块内二次判断,确认无实例,执行
new创建对象(volatile保证顺序与可见性)。4.创建完成,释放锁。
5.后续线程:第一层判断发现
instance != null,直接返回实例,不进锁。
4.2 组赛队列(一种更为复杂的队列,主要场景为"生产者消费者模型")
阻塞特性:
1.队列为空,尝试出队列,出队列操作就会阻塞。直到其他线程添加元素
2.队列为满,尝试入队列,入队列操作也会阻塞。直到其他线程取走元素
|----|---------------------------|
| 优点 | a)解耦合 b)削峰填谷 |
| 缺点 | a)引入队列后,整体结构更为复杂 b)效率有所影响 |
java
public void put(int value) throws InterruptedException {
synchronized (this) { // 加锁:保证队列操作的原子性
// 此处最好使用 while.
// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了
// 就只能继续等待
while (size == items.length) { // 队列已满,生产者需要等待
wait(); // 释放锁并进入等待状态,直到被消费者唤醒
}
// 队列未满,执行入队操作
items[tail] = value; // 把数据放到队尾位置
tail = (tail + 1) % items.length; // 队尾指针后移,循环队列处理
size++; // 队列元素数量+1
notifyAll(); // 唤醒所有等待的线程(包括消费者)
} // 同步块结束,自动释放锁
}
public int take() throws InterruptedException {
int ret = 0;
synchronized (this) { // 加锁:保证队列操作的原子性
while (size == 0) { // 队列为空,消费者需要等待
wait(); // 释放锁并进入等待状态,直到被生产者唤醒
}
// 队列不为空,执行出队操作
ret = items[head]; // 取出队头元素
head = (head + 1) % items.length; // 队头指针后移,循环队列处理
size--; // 队列元素数量-1
notifyAll(); // 唤醒所有等待的线程(包括生产者)
} // 同步块结束,自动释放锁
return ret; // 返回取出的元素
}
4.3 线程池(工厂模式)
线程池包含的线程数量是可动态调整的 ,使用线程池可以省下应用程序切换到内核中运行这样的开销,让我们高效的创建销毁线程
标准库中的线程池:1.使用Executor.newFixedThreadPool( )来创建线程池
2.核心方法submit(Runnable)
3.通过Runnable描述一段要执行的任务
4.通过submit任务放到线程池中
5.此时线程池里的线程就会执行这样的任务
java
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 本质上是 ThreadPoolExecutor 类的封装. ThreadPoolExecutor 提供了更多的可选参数, 可以进⼀步细化线程池行为的设定.

- int corePoolSize (核心线程数,至少有多少个线程,线程池一创建这些线程也创建,直到线程池销毁,这些线程才销毁)
- int maximumPoolSize(核心线程数+非核心线程数不忙就销毁,繁忙就创建)
- long keepAliveTime(非核心线程允许空闲最大时间)
- TimeUnit unit(枚举常见时间单位,可指定long keepAliveTime 时间)
- BlockingQueue<Runnable> workQueue(工作队列)
- ThreadFactory threadFactory(工厂模式通过静态方法,把构造对象new的过程,各种属性初始化过程封装起来来弥补构造方法缺陷,)
- RejectedExecutionHandler handler(拒绝策略)
- AbortPolicy 线程池直接抛异常,线程池可能无法继续工作
- CallerRunsPolicy 让调用submit的线程自行执行任务
- DiscardoldestPolicy 丢队列最老任务
- DiscardPolicy 丢队列最新任务
4.4 定时器
优先级队列(时间精度高),时间到了执行一些逻辑
实现:
1.创建一个类,表示一个任务
2.用集合类把多个任务管理起来
3.实现schedule方法,把任务添加到队列中
4.额外创建一个线程,负责执行队列中的任务
java
import java.util.concurrent.PriorityQueue;
// 1. 创建一个类,表示一个任务
class MyTask implements Comparable<MyTask> {
// 要执行的任务逻辑
private Runnable runnable;
// 任务的执行时间(毫秒时间戳)
private long time;
public MyTask(Runnable runnable, long delay) {
this.runnable = runnable;
// 当前时间 + 延迟时间 = 任务执行时间
this.time = System.currentTimeMillis() + delay;
}
public void run() {
runnable.run();
}
public long getTime() {
return time;
}
// 优先级队列需要实现 Comparable,让时间早的任务排在前面
@Override
public int compareTo(MyTask o) {
// 时间小的任务优先级更高(排在队首)
return Long.compare(this.time, o.time);
}
}
// 定时器类
class MyTimer {
// 2. 用集合类(优先级队列)管理多个任务
private final PriorityQueue<MyTask> queue = new PriorityQueue<>();
// 锁对象,用于线程间通信
private final Object locker = new Object();
public MyTimer() {
// 4. 额外创建一个线程,负责执行队列中的任务
Thread worker = new Thread(() -> {
while (true) {
try {
synchronized (locker) {
// 队列为空,等待任务加入
while (queue.isEmpty()) {
locker.wait();
}
// 取出队首任务(最早要执行的)
MyTask task = queue.peek();
long now = System.currentTimeMillis();
if (now >= task.getTime()) {
// 时间到了,执行任务
queue.poll();
task.run();
} else {
// 时间还没到,等待剩余时间
locker.wait(task.getTime() - now);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
worker.start();
}
// 3. 实现schedule方法,把任务添加到队列中
public void schedule(Runnable runnable, long delay) {
MyTask task = new MyTask(runnable, delay);
synchronized (locker) {
queue.offer(task);
// 添加任务后唤醒等待的线程,重新判断队首任务的时间
locker.notify();
}
}
}
// 测试代码
public class TimerDemo {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
System.out.println("开始执行定时任务:" + System.currentTimeMillis());
// 延迟 1000ms 执行的任务
timer.schedule(() -> System.out.println("任务1执行:" + System.currentTimeMillis()), 1000);
// 延迟 2000ms 执行的任务
timer.schedule(() -> System.out.println("任务2执行:" + System.currentTimeMillis()), 2000);
// 延迟 500ms 执行的任务
timer.schedule(() -> System.out.println("任务3执行:" + System.currentTimeMillis()), 500);
}
}
五,保证线程安全的思路
**1.**使用没用共享资源的模型
**2.**适用共享资源只读,不写的模型
a,不需要写共享资源的模型
b,使用不可变对象
3.直面线程安全
a,保证原子性
b,保证顺序性
c,保证可见性
六,线程和进程对比
6.1 线程的优点
1.创建一个新线程的代价比创建一个新进程的代价小很多
2.与进程间的切换相比,线程间的切换要操作系统做的工作很少
3.线程占用资源远小于进程
4.线程可以充分利用处理器的并行计算能力
5.可在等待慢速I/O时,执行其他任务(解决I/O阻塞问题)
6.计算密集型应用,可拆分任务,适配多处理器系统
7.I/O密集型应用,将I/O操作重叠,提高并发处理能力
6.2 线程和进程的区别
1.线程是操作系统中调度的基本单位,进程是资源分配的基本单位,线程共享进程的地址空间和资源
2.进程有自己的内存地址空间(独立),线程只独享指令流执行的必要资源(共享+私有)如寄存器和栈。
-
由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
-
线程的创建/切换/终止效率更高
七,小结
又拖拖拖,最近只忙着赶紧往后赶进度又好久没写博客了,心碎心碎。本来准备带小咪去绝育但是查出来小咪身体有点小问题更是心塞。我从明天开始必须好好沉淀,暑假找到实习,我还要考六级。我的天,为什么之前对自己那么好,现在一座座大山压下来要命了,沉淀沉淀吧,大家都加油都要好好敲代码!!!其实离我写前半段话已经过了十几天了哈哈哈,前面一段时间只顾着看课看课了,现在才写完,后面十几天会疯狂输出已经学了的,敬请期待吧!!!