1. 线程同步
多个线程操作同一个资源,
同一个对象被多个线程 同时操作,
- 同一个资源,多人都想使用
线程同步:是一种等待机制,
-
多个需要同时访问此对象的线程,
-
进入这个 对象的等待池,形成队列,
-
等待前面线程使用完毕,
-
下一个线程再使用。
线程同步:形成条件,队列+锁(对象本身)
- 在访问时 加入锁机制 synchronized
- 当一个线程 获得对象的排它锁,独占资源,
- 其他线程 必须等待,使用后 释放锁即可。
排它锁问题:
- 导致其他 所有需要此锁的线程 挂起。
- 多线程竞争下,加锁 释放锁 ,导致:比较多的 上下文切换 和 调度延迟
- 性能问题
- 优先级高的线程 等待一个优先级低的线程释放锁
- 导致 优先级倒置,性能问题
买票逻辑
public class BuyTicket implements Runnable {
//票
private int nums = 10;
@Override
public void run() {
while (nums > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + nums--);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
BuyTicket b = new BuyTicket();
new Thread(b, "线程1").start();
new Thread(b, "线程2").start();
}
}
线程2买到了票10
线程1买到了票9
线程1买到了票7
线程2买到了票8
线程2买到了票6
线程1买到了票6
线程2买到了票5
线程1买到了票5
线程2买到了票4
线程1买到了票3
线程1买到了票1
线程2买到了票2
-
每个线程 在自己的 工作内存交互,内存控制不当 会造成 数据不一致。
-
最后一个票,每个人都 会把 1 拿到 自己的工作内存,
- 第一个人买了 变成0,第二个变成了 -1
银行取款
public class UnsafeBank {
public static void main(String[] args) {
Account account = new Account(100, "结婚基金");
Drawing you = new Drawing(account, 50, "你");
Drawing girlFriend = new Drawing(account, 100, "girlFriend");
you.start();
girlFriend.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() {
//如果卡里的钱 - 取了多少钱 <0
if (account.money - drawingMoney < 0) {
System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
return;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//卡内余额 = 卡内余额 - 取了多少钱。同时操作这个,只有一个赋值成功的。
account.money = account.money - drawingMoney;
//你手里的钱
nowMoney = nowMoney + drawingMoney;
//打印余额
System.out.println(this.getName() + "取得" + account.name + "余额为:" + account.money);
//打印手里的钱
//Thread.currentThread().getName() 就是 this.getName()
System.out.println(this.getName() + "手里的钱为:" + nowMoney);
}
}
girlFriend取得结婚基金余额为:-50
//打印时 各自都 减去过,减去那一步 没有乱。所以为 -50
girlFriend手里的钱为:100
你取得结婚基金余额为:-50
你手里的钱为:50
你取得结婚基金余额为:50
你手里的钱为:50
girlFriend取得结婚基金余额为:50 //减去后变为了0,但打印时:被 你线程 100-50覆盖了ss
girlFriend手里的钱为:100
你取得结婚基金余额为:50
你手里的钱为:50
girlFriend取得结婚基金余额为:-50 //余额为50,又取了100
girlFriend手里的钱为:100
List线程不安全
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
list.add(Thread.currentThread().getName());
}).start();
}
try {
//必须要睡眠一下,等 开的线程执行完毕
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
//997
//如果为 1万。真实为:9996
// 添加到了 同一个位置
2. 线程同步
- Synchronize d,方法 和 块
- 控制对象的访问,每个对象 对应一把锁,
- 必须获得 调用该方法的 对象锁 才能执行,否则阻塞。
- 一旦执行,就独占该锁,直到 方法返回,才释放锁。
- 将一个方法 申明为 Synchronized将会影响效率
- 需要 修改的内容 才需要锁
买票问题
public class BuyTicket implements Runnable {
//票
private int nums = 5;
//停止标志
private boolean flag = true;
@Override
public void run() {
//如果为true,一直循环
while (flag) {
System.out.println("线程进入:" + Thread.currentThread().getName());
buy();
}
}
//加锁
private synchronized void buy() {
if (nums <= 0) {
System.out.println(Thread.currentThread().getName() + "没票了" + nums);
flag = false;
return;
}
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "买到了票" + nums--);
}
public static void main(String[] args) {
BuyTicket b = new BuyTicket();
new Thread(b, "线程1").start();
new Thread(b, "线程2").start();
}
}
System.out.print(" "); //打印一个空格,就能抢占了,否则 CPU太怪,线程1 一下买光了
线程1买到了票5
线程1买到了票4
线程2买到了票3
线程2买到了票2
线程2买到了票1
线程2没票了0
线程1没票了0
银行取款
synchronized (obj){
}
-
obj 称为 同步监视器
-
推荐使用 共享资源
-
同步方法,的 同步监视器 就是this,或者是 class
-
执行过程
- 锁定 obj,执行其中的代码
- 第二个线程访问,发现 同步监视器被锁定,无法访问
- 第一个线程 访问完毕,解锁 同步监视器
- 第二个线程访问,发现 已经没有锁了,然后锁定并访问。
run里的所有代码,都放入 代码块
synchronized (Account.class) {
}
synchronized (account) { //都一样,锁共享对象,变化的量,才行
}
锁 list
synchronized (list){
list.add(Thread.currentThread().getName());
}
CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Callable;
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10000; i++) {
Thread t = new Thread(() -> {
synchronized (list) {
list.add(Thread.currentThread().getName());
}
});
t.start();
try {
t.join(); //主线程等待,t线程强制执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(list.size());
源码
final transient ReentrantLock lock = new ReentrantLock(); //可重入锁
private transient volatile Object[] array;
transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。
volatile 可见性,就是改了值,立刻就变成最新的值。
可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
不保证原子性
transient
英
/ˈtrænziənt/
美
/ˈtrænʃ(ə)nt/
全球发音
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
adj.
转瞬即逝的,短暂的;暂住的,(工作)临时的
n.
暂住者,流动人口;(电流、电压、频率的)瞬变
volatile
英
/ˈvɒlətaɪl/
美
/ˈvɑːlət(ə)l/
全球发音
adj.
易变的,动荡不定的,反复无常的;(情绪)易变的,易怒的,突然发作的;(液体或固体)易挥发的,易气化的;(计算机内存)易失的
3. 死锁
互相等待 其他线程占有的资源 才能运行,
- 同步块 同时拥有 两个以上对象的锁,可能发生
代码
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0, "灰姑娘");
Makeup g2 = new Makeup(1, "白雪公主");
g1.start();
g2.start();
}
}
//口红
class Lipstick {
}
//镜子
class Mirror {
}
class Makeup extends Thread {
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;
String girlName;
Makeup(int choice, String girlName) {
super(girlName);
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
makeUp();
}
private void makeUp() {
if (choice == 0) {
synchronized (lipstick) {
System.out.println(this.getName() + " 获得口红的锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//放在里面,就死锁,放外面就是解决死锁。 相当于 这个 ==0 分支,放开锁,然后进入等待。
synchronized (mirror) {
System.out.println(this.getName() + " 获得镜子的锁");
}
} else {
synchronized (mirror) {
System.out.println(this.getName() + " 获得镜子的锁");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lipstick) {
System.out.println(this.getName() + " 获得口红的锁");
}
}
}
}
}
英
/ˈlɪpstɪk/
美
/ˈlɪpstɪk/
全球发音
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n.
口红,唇膏
mirror
英
/ˈmɪrə(r)/
美
/ˈmɪrər/
全球发音
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n.
镜子;写照,真实反映;(计算机)镜像网点;榜样
灰姑娘 获得口红的锁
白雪公主 获得镜子的锁 。尝试获得 口红,灰姑娘口红的锁 睡1秒就放开了。
白雪公主 获得口红的锁 。
灰姑娘 获得镜子的锁 。白雪公主,执行完毕,释放镜子的锁。灰姑娘拿到。
必要条件
- 互斥
- 一个资源,只能被 一个线程使用
- 请求与保持
- 请求资源而阻塞,对 已获得的资源 保持不放
- 不剥夺
- 已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待
- 进程之间 形成一种 头尾相接 的循环等待 资源关系
4. Lock锁
- jdk 1.5。线程同步机制。
- 通过 显式 定义 同步锁 对象来实现同步。
- 同步锁 使用 Lock对象 充当。
import java.util.concurrent.locks.Lock;
public interface Lock {
}
-
接口是 控制多个线程 对 共享资源 进行访问的 工具。
- 提供了 对共享资源的 独立访问,
- 每次只能 有一个线程 对 lock 对象加锁,
- 线程开始 访问 共享资源 之前 应 先获得 lock对象
-
ReentrantLock,还有 ReentrantReadWriteLock 可重入锁 Reentrant 英 /riːˈentrənt/ 美 /rɪˈentrənt/ 全球发音 简明 韦氏 例句 百科 adj. 再进去的;凹角的 n. 凹角;再进入
- 实现了 Lock,拥有 与syn chro nize d 相同的 并发性 和 内存语义
- 可以显式 加锁,释放锁
ReentrantLock
public class TestLock implements Runnable {
int ticketNums = 10;
private final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
TestLock test = new TestLock();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.start();
t2.start();
}
@Override
public void run() {
while (true) {
try {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
if (ticketNums > 0) {
System.out.println(Thread.currentThread().getName() + "买到了票" + ticketNums--);
} else {
break;
}
} finally {
// 一般写到 finally 里
lock.unlock();
}
}
}
}
Thread-0买到了票10
Thread-1买到了票9
Thread-0买到了票8
Thread-1买到了票7
Thread-1买到了票6
Thread-0买到了票5
Thread-0买到了票4
Thread-1买到了票3
Thread-1买到了票2
Thread-0买到了票1
-
Lock 是显式锁,手动开启 和 关闭
- 只有 代码块锁
- 性能更好,jvm将 花费较少的时间来 调度线程
- 扩展性好,更多的子类
-
synchronize d,是 隐式锁,出了作用域 自动释放
- 有代码块 和 方法锁
-
Lock > 同步代码块 > 同步方法
5. 线程协作
生产者 消费者
-
是一个问题,不是设计模式
-
有一个设计模式叫观察者,就是 注册到一个list里,改变后 通知所有list里的对象。
-
仓库只能存放一件产品,
- 生产者 生产出来的产品 放入仓库,
- 消费者 将仓库中 产品 取走消费
-
如果仓库没有产品,生产者 将产品放入仓库,否则等待
- 直到 产品被消费者 取走
-
如果 有产品,消费者 则可以 取走消费,否则等待
- 直到 仓库中 再次放入产品
-
producer
- 数据缓冲区
- consumer
- 数据缓冲区
-
生产者 和 消费者 共享同一个资源,并且 相互依赖。
-
生产者:
- 没有生产产品 之前,通知消费者等待。
- 生产了产品,通知 消费者 消费
-
消费者:
- 消费之后,通知 生产者,已经结束消费,需要 新产品
-
synchronize d 实现了 同步,
- 不能用来 实现 不同线程之间的消息传递(通信)
线程通信
-
所有对象都有一个锁
-
Object 类的方法,
- 只能在 同步方法 或 同步代码块中使用
- 否则抛出 ILLegal Monitor State Exception
- 只能在 同步方法 或 同步代码块中使用
ILLegal
英
/ɪˈliːɡ(ə)l/
美
/ɪˈliːɡ(ə)l/
全球发音
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
adj.
非法的,违法的;违规的
n.
非法移民,非法劳工
Monitor
英
/ˈmɒnɪtə(r)/
美
/ˈmɑːnɪtər/
全球发音
简明 牛津 新牛津 韦氏 柯林斯 例句 百科
n.
显示器,监控器;监视仪,监护仪;监督员,监察员;(学校里的)班长,级长;(电台的)监听员;扬声器;巨蜥;浅水重炮舰
wait() 线程一直等待,直到 其他线程通知,会释放锁, 而sleep是抱着锁睡。
wait() 指定等待的 毫秒数
notify() 唤醒 一个处于 等待状态的线程
notifyAll()唤醒 同一个对象上 所有调用 wait()方法的线程
优先级 高的线程,优先调度
并发协作模型:管程法
- 生产者
- 消费者
- 缓冲区:消费者 不能直接使用 生产者的数据,他们之间 有个缓冲区
- 生产者 将生产好的 数据放入 缓冲区
- 消费者 从缓冲区 拿出数据
- 这个池子,可以放 很多个 数据。
并发协作模型:信号灯法
- 标志位:
- 如果为真,就等待。
- 如果为假,就通知另外一个人。
管程法实现
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Producer(container).start();
new Consumer(container).start();
}
}
@AllArgsConstructor
class Producer extends Thread {
SynContainer container;
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
container.push(new Chicken(i));
System.out.println("生产了" + i + "只鸡");
}
}
}
@AllArgsConstructor
class Consumer extends Thread {
SynContainer container;
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
container.pop();
}
}
}
@AllArgsConstructor
class Chicken {
int id;
}
class SynContainer {
Chicken[] chickens = new Chicken[5];
int count = 0;//容器计数器
public synchronized void push(Chicken chicken) {
if (count == chickens.length) {
try {
//如果满了,生产者等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//没有满,丢入产品
chickens[count] = chicken;
count++;
this.notifyAll(); //this是SynContainer,所以 不加all更好,就是唤醒对方。
}
public synchronized Chicken pop() {
//判断count ==10或0的那一句,最好不要用if,应该用while,
//否则当有多个消费者的时候,会出现脏判断的
if (count == 0) {
try {
//没有鸡,等待生产者生产。消费者 进行等待
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//可以消费
count--;
//吃完一只鸡,通知生产者 生产
Chicken product = chickens[count];
System.out.println("消费了->" + product.id + "只鸡");
//吃完了,通知生产者 生产
this.notifyAll();
return product;
}
}
生产了1只鸡
生产了2只鸡
生产了3只鸡
生产了4只鸡
生产了5只鸡
消费了->5只鸡
消费了->4只鸡
消费了->3只鸡
消费了->2只鸡
消费了->1只鸡
生产了6只鸡
消费了->6只鸡
生产了7只鸡
消费了->7只鸡
生产了8只鸡
消费了->8只鸡
生产了9只鸡
消费了->9只鸡
生产了10只鸡
消费了->10只鸡
信号灯法
public class TestPc2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
//演员
@AllArgsConstructor
class Player extends Thread {
TV tv;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i % 2 == 0) {
this.tv.play("快乐大本营");
} else {
this.tv.play("抖音记录美好生活");
}
}
}
}
//观众
@AllArgsConstructor
class Watcher extends Thread {
TV tv;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
tv.watch();
}
}
}
class TV {
String voice;
//演员表演,观众等待 T
//观众观看,演员等待 F
boolean flag = true;
public synchronized void play(String voice) {
//如果不为真,演员等待
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() {
if (flag) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了" + voice);
//通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
演员表演了:快乐大本营
观看了快乐大本营
演员表演了:抖音记录美好生活
观看了抖音记录美好生活
演员表演了:快乐大本营
观看了快乐大本营
演员表演了:抖音记录美好生活
观看了抖音记录美好生活
演员表演了:快乐大本营
观看了快乐大本营
6. 线程池
callable
//1. 实现接口,需要返回值类型
public class MyCallable implements Callable<Boolean> {
//2. 方法 需要抛出异常。不重写带异常的 也行。
@Override
public Boolean call() throws Exception {
System.out.println(Thread.currentThread().getName() + "我还在听课");
//3. 创建 目标对象
return false;
}
public static void main(String[] args) {
//4. 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//5. 提交执行
Future<Boolean> r = ser.submit(new MyCallable());
System.out.println("随机执行的");
try {
//6. 获取结果
Boolean b = r.get();
System.out.println("必然在子线程之后执行");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//7. 关闭服务
ser.shutdown();
}
}
-
创建 和 销毁,使用量 特别大的资源,对 性能影响很大。
-
提前 创建好线程,放入线程池,使用时 直接获取,使用完放回。
- 避免 频繁创建和销毁,实现 重复利用。
- 类似:生活中的 公共交通工具。
-
好处:
- 提高 响应速度,减少了 创建新线程的时间,
- 降低 资源消耗,(不需要每次都创建)
- 便于线程管理:
- corePoolSize:核心池的 大小
- maxi mum PoolSize:最大线程数
- keepAliveTime:线程没有任务时 最多保持 多长时间 后终止。
JDK1.5 线程池:Executor Service 和 Executors
-
ExecutorService:真正的线程池接口,子类:ThreadPoolExecutor
- execute(Runnable c):执行任务,没有返回值
- submit(Callable task ):执行任务,有返回值
- shutdown():关闭连接池
-
Executors :工具类,线程池的工厂类,用于创建并返回 不同类型的线程池。
-
源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public class MyRunnable implements Runnable {
public static void main(String[] args) {
ExecutorService ser = Executors.newFixedThreadPool(3);
ser.execute(new MyRunnable());
ser.execute(new MyRunnable());
ser.shutdown();
}
@Override
public void run() {
System.out.println("子线程执行了");
}
}
FutureTask
- 运行 Callable的实现类
FutureTask<Boolean> f = new FutureTask(new MyCallable());
new Thread(f).start();
try {
Boolean o = f.get();
System.out.println(o);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}