一、Java线程的六种状态

就绪状态和阻塞状态是线程的两种常见的状态,而Java中又对线程作了进一步的区分,即Java中总共有六种线程状态:

  • 新建状态(New):线程对象被创建后,但还没有调用start()方法启动线程时,线程处于新建状态。
  • 可运行状态(Runnable):就绪状态我们可以理解为两种情况,一种情况是线程正在CPU上运行,另外一种情况就是线程正在排队中,随时可以去CPU上运行。
  • 阻塞状态(Blocked):因为锁的原因产生了阻塞。
  • 等待状态(Waiting):因为调用wait方法产生了阻塞。
  • TIMED_WAITING:因为调用sleep方法产生了阻塞。
  • 守护状态(Terminated):线程已经执行结束了,但是该线程对应的Thread对象还在。

注意,有的地方java线程分为了七种状态,即把可运行状态(runnable)拆分成了两种状态就绪状态(ready)和运行状态(running)。这个地方也是可以的。

二、多线程带来的安全问题——线程安全(重点重点)

class Counter {
    public int count = 0;
    public void increase() {
        count++;
    }
}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() ->{
            for(int i = 1;i <= 10000;i++) {
                counter.count++;
            }
        });
        Thread t2 = new Thread(() ->{
            for(int i = 1;i <= 10000;i++) {
                counter.count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在上述代码运行之前我们先来复习以下之前的lambda表达式的变量捕获问题:线程t1和t2都引用了外部的Counter对象counter,并且在各自的run()方法中对count进行了自增操作。我们虽然对counter对象中的成员变量进行了修改,但是我们并没有修改counter对象本身。注意线程t1和线程t2引用的是counter对象,而不是counter对象中的成员变量,所以这里依然可以触发变量捕获。

三、线程不安全问题的原因

  • 根本原因:多个线程的调度顺序是随机的,操作系统采用的是抢占式执行的策略来调度线程的。(我们以往只需要考略代码在一个固定的执行顺序下运行成功即可,但是我们现在要考虑的是代码需要在多线程中的所有执行顺序中都要正确执行代码才可以。)那现在我们能不能想个办法让代码按照多线程的一定执行顺序下进行执行呢?很遗憾,现在我们还不能很好的解决这个问题。(当前主流的操作系统都是按照抢占式的执行顺序来执行的)
  • 代码结构的原因:多个线程同时修改同一个变量,此时容易发生线程安全问题。(一个线程修改一个变量多个线程读取同一个变量多个线程修改多个变量都是没有问题的)。
  • 进行的修改操作不是原子性的(比如上述代码中的count++操作就不是原子的,因为count++操作实质上是三条执行指令来完成的。),反之我们如果我们的修改操作是原子性的,此时代码就不会产生线程安全问题。另外关于原子的:一条java语句不一定是原子的,也不一定只是一条指令,比如count++就不是原子的、=即直接赋值操作就是原子的if=即先判断再赋值操也不是原子的
  • 内存可见性引起的线程安全问题。
  • 指令重排序引起的线程安全问题。

四、解决线程不安全问题

关于如何解决线程不安全问题的话,最主要的一个切入点就是改变修改操作的非原子性,即将我们的修改操作改变成原子的。我们可以通过加锁操作将一组操作给打包成一个操作,即打包成一个原子的操作。
注意:这里的给线程加锁的操作不同于数据库事务的原子操作:事务的原子操作依靠的主要是回滚;而这里的原子操作通过给线程加锁的操作将线程之间进行互斥,即这个线程在执行任务的时候,其它线程是无法执行任务的。简单来说就是通过给线程加锁以实现在同一时刻的多个线程中,只有一个线程在执行任务

synchronized关键字

synchronized在进行加减锁操作的时候是以对象为维度进行展开的。

五、总结

  • 如果两个线程针对的是同一个对象进行加锁的话,此时就会产生阻塞等待,不会出现线程安全的问题。所以,必须多个线程对同一个对象加锁此时才有意义。
  • 如果两个线程针对的是不同的对象进行加锁的话,此时也就不会出现阻塞等待,会出现线程安全的问题。
  • 如果两个线程中,一个线程加锁了而另一个线程没有加锁,此时依然会出现线程安全的问题(单方面加锁相当于没有加锁)。

好了,本文到这里就结束了,希望友友们可以支持一下一键三连哈。嗯,就到这里吧,再见啦!!!
【Java系列】详解多线程(三)—— 线程安全(上篇)-LMLPHP

12-17 05:12