目录
线程的状态
之前讨论的“就绪”和“阻塞”都是针对系统层面上的线程的状态(相当于针对一个进程只有一个线程的情况),但更常见的情况是一个进程中包含了多个线程,所谓的状态,其实是绑定在线程上。
NEW
安排了工作,还未开始行动
把Thread对象创建好了,但还没开始调用start
public class Demo14 {
public static void main(String[] args) {
Thread t=new Thread(() ->{
});
System.out.println(t.getState());
t.start();
}
}
运行效果
TERMINATED
工作完了
就是操作系统的线程已经执行完毕,销毁了,但是Thread对象还在,获取到的状态。
public class Demo14_1 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(() ->{
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
执行效果
RUNNABLE
就绪状态
处于这个状态的线程,就是在就绪队列中,随时可以被调度到CPU上
如果代码中没有sleep,也没有其他可能导致阻塞的状态,代码大概率是处在Runnable状态的。
ublic class Demo14_2 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(() ->{
while(true){
//这里什么都不写
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
执行效果
TIMED_WAITING
代码中调用了sleep或者join(超时时间),就会进入到此状态。
意思是当前的线程在一定时间之内是阻塞的状态【一定时间到了之后,阻塞解除】。(阻塞状态之一)
public class Demo14_3 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(() ->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
}
执行效果
BLOCKED
当前线程在等待锁,导致阻塞(阻塞状态之一)
synchronized
WAITING
当前线程在等待唤醒,导致阻塞(阻塞状态之一)
线程状态转换图
线程安全
概念
操作系统,线程调度的时候,是随机的(抢占式执行),正是因为这样的随机性,就可能导致程序执行出现一些bug.
如果因为这样的调度随机性引入了bug,就认为线程是不安全的,如果是因为这样的调度随机性,也没有带来bug,就认为是线程安全的。
线程不安全的典型案例
使用两个线程,对同一个整型变量,进行自增操作,每个线程自增5w次,看最终的结果。
class Counter {
public int count;
public void increase(){
count++;
}
}
public class Demo15 {
private static Counter counter=new Counter();
public static void main(String[] args) throws InterruptedException {
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();
}
});
t1.start();
t2.start();
//必须要在t1 t2都执行完了之后,在打印count的结果
//否则,main 和 t1 t2之间都是并发执行的关系,导致t1 t2还没执行完,就先执行了下面的打印操作
t1.join();
t2.join();
//在main中打印一下两个线程自增完成之后,得到的count结果
System.out.println(counter.count);
}
}
执行效果
预期是10w,但实际是63826
这里累加的结果是5w到10w之间
这5w对并发相加中,有时候是串行的(+2),有时候是交错的(+1),具体串行多少次,交错多少次,是未知的
极端情况下:
如果所有的操作是串行的,此时结果就是10w
如果所有的操作是交错的,此时结果就是5w
原因:
如何解决上述问题?
加锁
加锁之后,就变成串行了,和单线程就没啥区别了。
但是在实际开发过程中,一个线程中药做很多任务,例如:线程里要执行步骤1 ,步骤2,步骤3 ,步骤4,其中很可能只有 步骤4才涉及到线程安全问题,针对步骤4加锁即可,此时上面的步骤1 ,步骤2,步骤3 都可以并发执行。
如何加锁
使用synchronized 关键字
class Counter {
public int count;
synchronized public void increase(){
count++;
}
}
执行效果
给方法直接synchronized关键字,此时进入方法,就会自动加锁,离开方法,就会自动解锁。
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程,就处在BLOCKED状态),阻塞会一直持续到占用锁的线程把锁释放为止。
线程不安全的原因
不是所有的多线程都要加锁(如果这样,多线程的并发能力就形同虚设)
1.线程是抢占式执行的,线程间的调度充满随机性【线程不安全的万恶之源】根本原因,无可奈何
2.多个线程对同一个变量进行修改操作(如果是多个线程针对不同的变量进行修改,没事;如果是多个线程针对同一个线程读,也没事)通过调整代码结构,使不同线程操作不同变量
3.针对变量的操作不是原子的
有些操作,比如读取变量的值,只是针对一条机器指令,此时这样的操作本身就可以视为是原子的。
通过加锁操作,也就是把好几个指令给打包成一个原子的了。
4.内存可见性,也会影响到线程安全。
public class Demo16 {
private static int isQuite=0;
public static void main(String[] args) {
Thread t=new Thread(() ->{
while(isQuite==0){
}
System.out.println("循环结束!t线程退出");
});
t.start();
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个isQuit的值:");
isQuite=scanner.nextInt();
System.out.println("main线程执行完毕");
}
}
执行效果
此时,这里没有输出这句话。System.out.println(“循环结束!t线程退出”);
那么如何解决呢?
(1)使用synchronized关键字
synchronized不光能保证指令的原子性,同时也能保证内存可见性。被synchronized包裹起来的代码,编译器就不敢有上述的行为!相当于手动禁用了编译器的优化!
(2)使用volatile关键字
volatile和原子性无关,但能保证内存的可见性。
禁止编译器做出上述优化。编译器每次执行判定相等的时候,都会重新从内存中读取isQuite的值
private static volatile int isQuite=0;
执行效果
5.指令重排序,也会影响到线程安全问题
指令重排序,也是编译器优化中的一种操作。
那么,如何解决这样的问题呢?
使用synchronized关键字
synchronized不仅能保证原子性,同时也能保证内存可见性,同时还能禁止指令重排序。
synchronized的用法
synchronized翻译为同步的。
“同步”在计算机中存在多种意思。
在多线程中,线程安全中,同步指“互斥”。
在I/O或者网络编程中,同步相对的词叫“异步”,此处的同步和互斥没有关系和线程也没有关系,表示的是消息的发送方如何获取到结果。
直接修饰普通方法
使用synchronized的时候,本质上是针对某个“对象”进行加锁。
修饰一个代码块
需要显示指定针对哪个对象加锁(Java中的任意对象都有锁对象)
class Counter {
public int count;
public void increase(){
synchronized (this){
count++;
}
}
}
public class Demo15 {
private static Counter counter=new Counter();
public static void main(String[] args) throws InterruptedException {
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();
}
});
t1.start();
t2.start();
//必须要在t1 t2都执行完了之后,在打印count的结果
//否则,main 和 t1 t2之间都是并发执行的关系,导致t1 t2还没执行完,就先执行了下面的打印操作
t1.join();
t2.join();
//在main中打印一下两个线程自增完成之后,得到的count结果
System.out.println(counter.count);
}
}
修饰一个静态方法
相当于针对当前类对象加锁。