目录
1.线程的状态
首先明白进程的状态:就绪或者阻塞
上述说的就绪和阻塞其实是针对系统中的线程状态(PCB)
Java中对于Thread类中的线程的状态进行了进一步的细化
NEW: Thread对象有了,但是线程还没有被执行
TERMINATE:线程无了,但是Thread对象还在
Time_Waiting sleep(1000) 和join(1000) 导致的有限时间的阻塞
Waiting 等待唤醒导致的阻塞
Blocked 加锁导致的阻塞synchronized
2.线程安全问题
什么是线程不安全?
由于操作系统调度线程是随机的(抢占式执行)
所以由于随机性的调度线程可能会出现一些bug,这些我们成为线程不安全
线程安全不安全指的是有没有bug
上述是线程不安全的一个例子。
因为count++执行有三部(3个CPU指令),
1.获取count的值到寄存器
2.在寄存器中的值加1
3.寄存器中的值返回count的值
而如果不加锁,多个线程是抢占式执行的,他们同时可能都抢到了同样的一个值的count,假若说为100,那么他们加1后都是101,count进行了两个不同线程的一次++,本应该+2,却+了1,这就是线程不安全,给increase函数上锁,同时只能一个线程进入这个函数,就会解决这个问题
加锁解决线程安全问题
Object lock =new Object()
synchronized(lock){
//执行的加锁代码
}
加锁的机制,只有一把锁,一个线程加锁了后面的线程再加锁就会阻塞,直到加锁的那个线程执行完毕unlock。
通过阻塞,可以使一个乱序的并发变成一个串行操作,串行和单线程没啥区别了。
并发性越高,速度越快,但同时可能会导致线程不安全的bug
加了锁之后并发程度降低,速度变慢了。但是这是必要的牺牲。
多线程仍然是有非常大的意义的。实际开发中,一个线程会有许多的任务,他们的少部分是加锁的,但是剩下的大部分都是并发的,比串行还是快的多的。
加锁的方式
Java中加锁的方式有很多种,最多的就是synchronized关键字
1.直接给方法前面加synchronized
当一个线程加锁了之后,其他线程再加锁,就会处于阻塞状态(Blocked状态)阻塞一直持续到占用锁的线程Unlock
2.new个方法 synchronized(lock){代码体}
什么时候会线程不安全?
并不是所有的线程都要加锁,加锁了之后就和串行执行一样了(多线程的优势就形同虚设了)
线程不安全的原因:
1.线程是抢占式随机执行的线程是抢占式执行同一个资源
2.多个线程对同一个变量进行修改操作 ,多个线程对不同变量修改没事,多个线程对同一个变量读也没事
3.针对变量的操作不是原子的~(也就是说虽然只是一个行代码,但是cpu执行分为好几个步骤 如count++)
4.内存可见性:线程一对内存中的数据进行修改的时候,线程2可能感受不到,这就会导致线程不安全。
编译器对程序的优化可能会导致线程误判(线程2感受不到线程1的修改)
解决内存可见性方法:
1.synchronized加锁,被它加锁,编译器不会假设一个变量不会改变。
synchronized既保证了内存可见性,又保证了原子性
2.volatile 关键字
volatile关键字只保证了内存可见性,和原子性无关。
使用volatile 关键字对变量进行修饰,就是禁止编译器进行优化,每次读取数据只从 内从中去读取。
补充:编译器优化(很玄学),如果读的操作,编译器发现每次从内存中读的都是一个数据不变,重复很多很多次,它就会从寄存器中去读取数据(为了更快),但是后面一旦这个内存中的数据被修改,编译器感受不到,就导致了线程不安全。
补充:原子性:要么全部一起执行完毕,要么一个也不执行。count++这个不具备原子性,它的三部可以被不同的线程打乱
加锁操作就会把多个机器指令打包成一个原子的操作(一起执行的操作)
5.指令重排序,也会影响到线程安全问题,指令重排序也是编译器优化的一种操作
(就是对代码顺序的重新排序,使之更加高效(保证逻辑不变得前提下,再去调整顺序).一般不影响代码的结果,但是在多线程中可能会出问题)
解决方案:synchronized :不仅能保证原子性,还能保证内存可见性,还能保证指令重排序
3.synchronized的具体用法
synchronize 原意是同步,同步在计算机中有许多的意思.
在多线程的线程安全中,同步指的是"互斥"
在IO或者网络编程中,同步叫做"异步",此处的同步和互斥没有任何关系,和线程也没关系了.
使用方式:
1.直接修饰普通的方法
针对当前对象进行加锁
2.修饰一个代码块
3.修饰一个静态的方法
加锁的底层: