多线程
线程是一个独立的执行流
解决 并发编程 这样的问题,
Thread类.
构造方法:创建线程
五种写法:
继承Thread,实现Runnavble,继承Thread(匿名内部类),实现Runnable(匿名内部类),lambda表达式构造方法中还能指定线程的名字
创建线程 (系统中创建) start
中断线程 核心思路让线程run方法执行完
等待线程 join
获取线程引用 currentThread 哪个线程调用这个方法,就获取哪个线程的Thread实例
休眠线程 sleep
进程是有状态的
就绪
阻塞
(相当于是针对一个进程中只有一个线程或多个线程的情况)
对于在Java中Thread类中,对于线程的状态,又进一步的细化了
把Thread对象创建好了,但是还没有调用start
操作系统中的线程已经执行完毕,销毁了,
但是Thread对象还在,获取到的状态。
就绪状态
处于这个状态的线程,就是在就绪队列中
随时可以被调度到CPU上
如果代码中没有进行sleep,也没有进行其他的可能导致阻塞的操作
代码大概率处在Runnable状态的
代码中调用了sleep,就会进入到TIMED_WAITING
意思就是当前的线程在一定时间内,是阻塞的状态
Thread t =new Thread(()->{
while(true){
//这里啥都不能有
try{
Thread.sleep(millis:1000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
});
一定时间到了之后,阻塞状态解除,这种情况就是TIMED_WAITING
也是属于阻塞状态之一
BLOCKED:这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞(组赛状态之一) synchronized
WAITING:这几个都表示排队等着其他事情
当前线程在等待唤醒,导致了阻塞(阻塞状态之一)wait
线程安全 问题
所谓安全即没有
bug
一个线程不安全的典型案例:
使用两个进程,对同一整形变量,进行自增操作
每个线程自增5w次,看最终结果
t1.join();
t2.join();
这俩谁在前,谁在后都没关系
假设t1先结束 假设t2先结束
t1结束调动t2,等t2结束 先执行t1等t1结束,t2结束,t1还没结束
t2结束,t2执行完毕 main线程仍阻塞在t1中,再过一会儿t1结束,t1返回,执行t2,
此时t2结束了,t2就会立即返回
Thread t1 = new Thread(()->{
for(int i=0:i<50000;i++{
counter.increase();
}
});
Thread t2 = new Thread(()->{
for(int i=0:i<50000;i++{
counter.increase();
}
});
count++到底干了点啥
count++
1.把内存中的count的值,加载到CPU寄存器中 load
2.把寄存器中的值,给+1 add
3.把寄存器的值协会到 内存的count中 save
什么样的代码会产生这种线程不安全问题呢?
不是所有的多线程代码都要加锁(这样使多线程的并发能力形同虚设)
产生线程不安全的原因
1.线程是抢占式执行,线程的调度充满随机性
2.多个线程对同一个变量进行修改操作(如果是多个线程针对不同的变量或对同一个变量进行修改,都没事)
3.针对变量的操作不是原子的
(讲数据库事务)
针对有些操作,比如读取变量的值,只是对应一条 机器指令,此时这样的操作本身就可以视为是原子的
通过加所操作,也是把好几个指令给打包成一个原子的
4.内存可见性,也会影响到线程安全
比如,针对同一个变量
一个线程进行读操作(循环进行很多次)
一个线程进行修改操作(合适的时候执行一次)
t1这个线程
在循环读这个变量
按照之前的介绍
读内存操作,相比于读寄存器
是一个非常低效率的操作
1.使用synchronized关键字
(synchronized不光能保证指令的原子性,同时也能保证内存可见性)
被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设
相当于禁用了编译器的优化
2.使用volatile和原子性无关,但能够保证内存可见性
禁止编译器做出上述优化,编译器每次执行判定条件相等,都会重新从内存读取isQuit的值!!!
5.指令重排序,也会影响到线程安全问题
指令重排序,也是编译器优化中的一种操作
编译器会智能的调整这里代码的前后顺序,从而提高程序的效率。
保证逻辑不变的前提,再去调整顺序
如果代码是单线程的程序,编译器的判定一般都是很难准的
如果代码是多线程的,编译器可能产生误判。
synchronized的使用方式:
1.直接修饰普通的方法
使用synchronized的时候
使用synchronized的时候,本质上是在针对某个“对象”进行加锁
此时锁对象就是this
class Counter{
//这个 变量 就是两个线程要去自增的变量
public int count;
synchronized public void increase(){
count++;
}
}
当两个线程同时尝试对同一个对象加锁的时候,才有 竞争 如果是两个线程在针对不同对象加锁,就没有竞争
2.修饰一个代码块
需要显示指定针对哪个对象加锁(java中的任意对象都可以作为锁对象)
这种随手拿个对象都能作为锁对象的用法,这是java中非常有特色的设定
3.修饰一个静态方法
相当于针对当前类的类对象加锁