线程安全
成因:多个线程同时操作同一个数据 并且其中有数据的修改更新 大概率会出现数据异常问题
(数据在内存中修改更新的速度 慢于CPU在各个线程间切换的速度)
结果:出现重复数据 出现并不存在的数据
解决方案一:
添加同步代码块 将线程更新数据的代码保护起来 没有执行完毕前 其他线程无法进入
效果:某一个线程未执行完毕同步代码块内部代码之前 其他线程一直处于就绪状态 且获取不到CPU
实现逻辑:
1.某个线程获取CPU时间片段后 进入同步代码块 同时 会获得一把对象锁(标记) 带着锁进入代码块
2.期间 其他线程获取CPU时间片段后 也预备进入代码块 发现锁不存在 无法进入
3.线程执行完必代码块 退出时 会将锁归还 同时进入就绪状态 与其他就绪状态的线程一起 再次开始争抢CPU时间片段 直到有一个线程获取锁对象进入代码块
案例三:两人同时买100张票 显示第几个人买到第几张票
Runnable r = new Runnable() {
Object o = new Object();//所有线程共享的一把对象锁
int count = 100; //所有线程共享的票
@Override
public void run() {
// TODO Auto-generated method stub
String name = Thread.currentThread().getName();
while (true) { //通过循环控制线程结束与否
try {
Thread.sleep(200); //休眠增加线程的交叉
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o) { //同步代码块 保护数据的更新
if(count>0) { //预备买票前 判断是否还有余票
System.out.println(name + "获取第" + (101 - count) + "张票");
count--;
}else {
break; //退出大循环 结束当前线程任务
}
} //退出同步代码块 归还锁对象 进入就绪状态
}
}
};
new Thread(r, "张三").start();
new Thread(r, "李四").start();
练习:
四个线程 int j =0 两个对变量j进行+1操作 两个对j进行-1操作 展示每个线程操作后变量j的值
对象锁
所有线程【共用一把】对象锁 一般可以使用this 但因为所代表的的本类对象内的资源过多 可能过大 因此可声明Object o = new Object作为对象锁
解决方案二
同步方法 public synchronized 返回值 方法名(参数列表){将要同步的代码块放入其中} run中调用该方法
public class Nums implements Runnable {
int j =0;
Object o = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
show();//是一个同步方法 只要有线程在内部执行 其他线程就无法进入
}
public synchronized void show() {
String name = Thread.currentThread().getName();
if(name.equals("1")||name.equals("2")) {
j++;
}else {
j--;
}
System.out.println(name+":"+j);
}
}
解决方案三
方法中的同步代码块
//普通方法中 //run方法调用这个普通方法 线程遇到同步代码块 就会判断是否有锁
public void show() {
String name = Thread.currentThread().getName();
//放入同步代码块
synchronized (o) {
if(name.equals("1")||name.equals("2")) {
j++;
}else {
j--;
}
System.out.println(name+":"+j);
}
}
解决方案四
同步对象锁 Lock
Lock l = new ReentrantLock();//创建Lock锁对象 提供lock() unlock()
public class Nums implements Runnable {
int j =0;
Lock l = new ReentrantLock();//创建Lock锁对象
@Override
public void run() {
// TODO Auto-generated method stub
String name = Thread.currentThread().getName();
l.lock();//上锁
if(name.equals("1")||name.equals("2")) {
j++;
}else {
j--;
}
System.out.println(name+":"+j);
l.unlock();//解锁
}
}
死锁
两个线程分别拿到了对方继续执行所必须的对象锁 导致两个线程都无法继续执行(也不会主动释放自己的锁)
死锁条件:
1.互斥原则:一个资源(锁对象)只能被一个线程对象锁使用
2.请求与保持:一个线程因请求资源而阻塞时 另一个线程保持资源不释放
3.不剥夺原则:线程已经获取的资源 再未使用完毕前 不可被强制剥夺
4.循环等待:若干线程之间 形成一种首尾相接 互相等待资源的关系
Object o1 = new Object();
Object o2 = new Object();//两个锁对象
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o1) {
System.out.println(Thread.currentThread().getName()+"进入模块一");
synchronized (o2) {
System.out.println(Thread.currentThread().getName()+"获取锁2");
}
}
synchronized (o2) {
System.out.println(Thread.currentThread().getName()+"进入模块二");
synchronized (o1) {
System.out.println(Thread.currentThread().getName()+"获取锁1");
}
}
}
生命周期
1.新生状态:通过new Thread() 此时已经有对应的栈空间和资源 但是还没有开始执行
2.就绪状态:通过.start() 启动线程 一切准备就绪 只需要获取CPU时间片段 即可执行
3.运行状态:获取到了CPU时间片段 开始执行线程所绑定的任务
4.阻塞状态:正在执行的线程 遇到特殊情况被挂起 让出CPU使用权限 一旦阻塞结束 重新回到就绪状态
5.死亡状态:通过利用结束循环或者判断 条件为假 来结束线程任务
线程常用方法
1.休眠方法 .sleep(long 毫秒)
注意:
1.主动让出CPU 设定时间后 重新进入就绪状态
2.抱着锁睡觉 如果休眠代码处于同步代码块内部 则只是让出CPU 不会释放所对象
3.需要抛出一个InterruptedException编译期异常
2.线程打断 当前线程对象.interrupt()
注意:不会终止线程任务的执行 可以提前结束休眠状态 继续向下执行
3.线程礼让 .yield() 当前线程让出CPU 增加其他线程执行的几率 但是不绝对
4.强制执行 .join() 强制执行完毕加入的线程任务后 再继续执行原始线程任务
//获取主线程对象
Thread t1 = Thread.currentThread();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 'a'; i<='z';i++) {
System.out.println((char)i);
if((char)i=='h') {
//让出CPU
//Thread.yield();
try {
t1.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}).start();
Thread.sleep(3000);
System.out.println("主线程优先");
5.线程通信 等待唤醒
案例:
消费者 顾客线程 要购买包子 等待包子制作 生产者 老板线程 制作包子 并唤醒顾客来吃包子
锁对象.wait() //当前线程执行等待 让出CPU 释放锁对象
锁对象.notify() //唤醒当前锁对象标记的等待线程
锁对象.notifyAll() //唤醒当前锁对象标记的所有等待线程
Object o = new Object();//消费者和生产者 使用同一个锁对象
//消费者任务
Runnable r1 = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o) {
System.out.println("老板 来笼包子");
try {
o.wait();//通过锁对象 标记处于等待的线程
//当前线程 让出CPU 让出锁对象
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//唤醒后 从等待位置向下执行
System.out.println("包子真好吃");
}
}
};
//消费者任务
Runnable r2 = new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o) {
System.out.println("老板 要三笼包子");
try {
o.wait();//通过锁对象 标记处于等待的线程
//当前线程 让出CPU 让出锁对象
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//唤醒后 从等待位置向下执行
System.out.println("包子打包 走咯");
}
}
};
//三个消费者开启线程
new Thread(r1).start();
new Thread(r2).start();
new Thread(r1).start();
Thread.sleep(1000);//控制老板线程最后开启
//生产者任务设计并绑定线程 开启
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
synchronized (o) {
System.out.println("5秒后 包子出笼");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//o.notify();//唤醒一个线程
o.notifyAll();//唤醒当前锁对象标记的所有等待线程
System.out.println("包子好了 过来吃");
}//生产者释放锁对象 消费者才能获取锁对象
}
}).start();
注意:
1.至少两个线程 实现线程通信
2.两个线程需要使用【同一个】对象锁
3.唤醒后从等待位置 向下执行
4.多个等待线程 唤醒顺序与等待顺序相反
5.等待过程中 会让出CPU 释放锁对象
练习:
员工A到了 开始喝茶等待 员工B到了 开始聊天等待 老板到了 开始上菜 开餐 A开动了 B开动了
休眠与等待
1.所属的类不同 休眠方法 属于 Thread类 等待方法 属于Object类
2.休眠尽量不在同步代码块中 等待必须存在锁对象
3.休眠会让出CPU 不释放锁对象 等待会让出CPU 释放锁对象
4.休眠时间一到自动进入就绪状态 等待线程只有被唤醒后进入就绪状态
同步屏障
达到要求的线程数量前 所有线程会在标记节点等待 直到数量达到 被唤醒 开启下一步任务
案例:五个人到达后 才能开始吃饭
CyclicBarrier对象.await() 当前线程等待 并且将等待线程的数量更新
CyclicBarrier对象.getNumberWaiting() 获取对象中已有线程数量
//设定同步屏障 可以设定最少线程数量 达标后执行的任务(可要可不要 一定在达标后第一时间执行)
CyclicBarrier cb = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("人到齐了 合张影");
}
});
//等待线程的任务
Runnable r = new Runnable() {
Random random = new Random();//可以作为对象锁
@Override
public void run() {
// TODO Auto-generated method stub
String name = Thread.currentThread().getName();
try {
Thread.sleep(random.nextInt(2000));
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
synchronized (random) {
System.out.print(name+"到达现场");
System.out.println("还差"+(4-cb.getNumberWaiting())+"人");
}
try {
cb.await();//当前线程开始等待 此时将当前线程数量更新
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//唤醒后执行的代码
System.out.println(name+"开始吃饭");
}
};
//开启五个线程 绑定唯一的任务
for(int i =0;i<5;i++) {
new Thread(r,(i+1)+"号").start();
}
信号量
设定同时访问当前任务的最大线程数量 超出的线程不能绑定任务
控制流量 可以类似参考 线程数量固定的线程池
案例:两个窗口办理业务 显示进入和离开
//设定该任务最多线程数量
Semaphore s = new Semaphore(2);
Runnable r = new Runnable() {
Random random = new Random();
@Override
public void run() {
// TODO Auto-generated method stub
try {
s.acquire();//判断当前任务中是否已经有足够的线程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String name = Thread.currentThread().getName();
System.out.println(name+"进入了窗口");
try {
Thread.sleep(random.nextInt(2000));
} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
System.out.println(name+"离开了窗口");
s.release();//更新对象中线程的数量
}
};
for(int i=0;i<10;i++) {
//创建了十个线程对象
new Thread(r,(i+1)+"号").start();
}
//如果采用线程池解决这个道题 直接设定线程池中 只有两个线程