具体代码案例如下:
public class Demo1 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
System.out.println("hello thread");
});
System.out.println(t.getState());
}
}
把Thread对象创建好了,但是还没有调用start~执行结果如下:
使用t.getState()获取线程的状态~
2.TERMINATED:工作完成了public class Demo2 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t.getState());
}
}
操作系统中的线程已经执行完毕销毁了,但是Thread对象还在,此时获取到的状态就是TERMINATED.
3.RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作就绪状态.处于这个状态的线程,就是在就绪队列中.随时可以被调度到CPU上~~
如果代码中没有进行sleep,也没有进行其他的可能导致阻塞的操作,代码大概率是处在RUNNABLE状态的~~~
public class Demo3 {
public static void main(String[] args) {
Thread t = new Thread(() ->{
});
t.start();
System.out.println(t.getState());
}
}
4.TIMED_WAITING:这几个都表示排队等着其他事情
代码中调用了sleep,就会进入到TIMED_WAITING,join(超时时间)
意思是当前的线程在一定时间之内是阻塞状态~~
public class Demo4 {
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(5000);
System.out.println(t.getState());
}
}
5.BLOCKED:这几个都表示排队等着其他事情
当前线程在等待锁,导致了阻塞.synchronized
6.WAITINT:这几个都表示排队等着其他事情当前线程在等待唤醒,导致了阻塞.wait
二.线程状态转换简图 三.线程安全问题操作系统,调度线程的时候是随机的~~(抢占式执行)正是因为这样的随机性,就可能导致程序的执行出现一些bug~如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的!!!
典型案例:
class Counter{
public int count;
public void increase(){
count++;
}
}
public class Demo5 {
public 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 j = 0; j < 50000; j++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
显然输出结果与预期结果10W存在差距. 此时程序就存在bug~~
问题原因:t1和t2是并发执行的,因此存在可能t1和t2在同一时刻对count进行自增,此时count就只会自增一次~~
四.对上述典型案例进行深入探究count++到底干了啥???
站在CPU的角度来看待,count++实际上是三个CPU指令!!!
正因为前面说的"抢占式执行",这就导致两个线程同时执行这三个指令的时候,顺序上充满了随机性~
这5w对并发相加中,有时候可能是串行的(+2),有的时候是交错的(+1)
具体串行的有多少次,交错的有多少次,都是随机的~~
极端情况下:(都是极小概率情况下,如果发生,那你可以去买彩票了)
所有操作都是串行的,此时结果就是10w;
所有操作都是交错的,此时结果就是5w.
五. 多线程--解决线程安全问题~解决上述问题的手段----加锁!!!
通过加锁,就限制了一次只能由一个线程来操作~
刚才t1已经把锁给占用了,此时t2尝试lock就会发生阻塞~lock会一直阻塞,直到t1线程执行了unlock,
通过这里的阻塞,就把乱序的并发,变成一个串行操作.
并发性越高,速度越快,但是同行可能会出现一些问题,加了锁之后,并发成都就降低了,此时数据就更靠谱了,速度也就慢了~~~
Java中加锁的方式有很多种,最常用的是synchronized这样的关键字
给方法直接加上synchronized关键字,此时进入方法,就会自动加锁,离开方法,就会自动解锁~~~
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待.(此时对应的线程,就处在BOLCKED状态)阻塞会一直持续到,占用锁的线程把锁释放为止~~~
class Counter{
public int count;
synchronized public void increase(){
count++;
}
}
加锁之后的执行结果就是10w!!!
六.产生线程不安全的原因(一)线程是抢占式执行,线程间的调度充满随机性.
(二)多个线程对同一个变量进行操作(如果是多个线程针对不同的变量进行修改,就没事!如果多个线程针对同一变量读,也没事!!!)
(三)针对变量的操作不是原子的~
针对有些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的~~
通过加锁操作,也就是把好几个指令打包成一个原子的操作.
(四)内存可见性,也会影响到线程安全!!!
例子:针对同一个变量,一个操作(t1)进行读操作(循环进行很多次),一个线程进行修改操作(t2)(合适的时候执行一次)
此时一旦t1做出了大胆的假设,此时t2修改了count,t1就不能感知到了
public class Demo1 {
public static int isQuit = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
while (isQuit == 0){
}
System.out.println("循环结束,t线程结束");
});
t1.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入isQuit的值:");
isQuit = scanner.nextInt();
System.out.println("main线程执行完毕");
}
}
此时查看执行结果:
此时main线程修改了isQuit的值,t1线程仍然执行循环~~~
主要原因如下图:
此时解决上述问题,有两种处理方式:
1.使用synchronized关键字
synchronized不光能保证指令的原子性,同时也能保证内存可见性~~
被synchronized包裹起来的代码,编译器就不敢轻易的做出上述假设~~相当于禁用了编译器优化~~
2.使用volatile关键字
volatile和原子性无关,但是能够保证内存可见性.禁止编译器做出上述优化,编译器每次执行判定相等,都会重新从内存读取isQuit的值~~~
(五).指令重排序,也会影响到线程安全问题~~~
指令重排序,也是编译器优化中的一种操作~~
编译器就会智能的调整这里代码的前后顺序,从而提高程序的效率~~
保证逻辑不变的前提,再去调整顺序~~
如果代码是单线程的程序,编译器的判定一般都是很准~~
如果代码是多线程的,编译器也可能产生误判~~`
synchronized不光能保证原子性,同时还能保证内存可见性,同时还能禁止指令重排序~~
七.synchronized的用法synchronized(同步的)在多线程中,线程安全中,同步是指"互斥"
使用方式:
1.直接修饰普通的方法.
使用synchronized的时候,本质上是在针对某个"对象"进行加锁~~
当两个线程同时尝试对同一个对象加锁的时候,才有竞争.如果两个线程在针对两个不同的对象加锁,就没有竞争~~
2.修饰一个代码块
需要显式指定针对哪个对象加锁.(Java中的任意对象都可以作为锁对象)
3.修饰一个静态方法
相当于针对当前类的类对象加锁



