线程状态及线程安全性

线程的状态

查看线程的所有状态

线程的状态是一个枚举类型 Thread.State
我们可以通过foreach的方法来查看Thread中包含哪几种状态

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
		}
	}
}
状态说明
NEW安排了工作,还未开始启动
RUNNABLE可工作的,可分为就绪状态和工作中
BLOCKED等待
WAITING等待
TIMED_WAITING等待
TERMINATED工作完成

请添加图片描述

  • BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其他线程发来通知
  • TIMED_WAITING线程在等待唤醒,但设置了时限;WAITING线程在无限等待唤醒
  • yield()短暂让出CPU

线程安全问题

线程不安全的例子

public class ThreadState {
    static class Counter{
        int count = 0;
        void increase(){
            count++;
        }
    }
    public static void main(String[] args) {
        final Counter counter = new Counter();

        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();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter.count);
    }

运行上述代码,我们会发现得到的结果总是小于100000,而我们预期得到的结果应该是100000,这种情况引出了本节要讨论的重点——线程的安全性。
如果在多线程中代码运行的结果与单线程环境得到的结果(单线程为应该得到的结果)不一致,则说这个程序是线程不安全的。
接下来,我们将会讨论线程不安全的原因。

线程不安全的原因

1.修改共享数据

在上述线程不安全的例子中,t1t2同时对counter.count数据进行修改,即在t1对数据进行读写的时候,t2对数据也在进行读写。
举个例子,t1读取内存中的数据10,同时t2也读取内存中的数据10,然后他们同时加1,再写入内存中,t1写入内存的是11,t2写入内存的也是11,这样内存中的数据便是11,按照预期的结果,做了两次加法,存入内存的数据应该是12。这样便造成了线程的不安全

2.原子性

原子性是指一系列操作的一个整体性,不可以打乱,也不可以切割执行某一部分。
在上述例子中,t1在读写数据的时候t2也在进行读写,这就导致了线程的不安全。
值得注意的是:
一条java语句不一定是原子的,也不一定只是一条指令。
比如n++,其实是由三步操作组成的:

  • 1.从内存把数据读到CPU
  • 2.进行数据更新
  • 3.把数据写回道CPU

3.可见性

可见性是指一个线程对共享变量值的修改,能够及时的被其他线程看到。

public static void main(String[] args) {
    int n = 0;
    for (int i = 0; i < 50000; i++) {
        n++;
    }
}

观察上面这段代码,这段代码大量重复着读内存,更新数据,写内存的操作,这时编译器会自动进行优化,更新为读内存,更新数据,更新数据。。。写内存的操作。
在单线程的情况下,这么优化没有任何问题。但在多线程的情况下,则可能出现内存中的数据不及时更新的问题。

4.代码重排序

一段代码是这样的顺序:1->2->3
编译器可能会对代码顺序进行优化,而变成1->3->2

线程安全问题的解决

synchronized关键字-监视器锁monitor lock

synchronized的特性
1)互斥

synchronized会起到互斥的效果,当某个线程执行到某个对象的synchronized中时,其他线程如果执行到同一个对象的synchronized就会阻塞等待

2)刷新内存

synchronized的工作过程:

  • 1.获得互斥锁
  • 2.从主内存拷贝变量的最新副本到工作的内存
  • 3.执行代码
  • 4.将更改后的共享变量的值刷新到主内存
  • 5.释放互斥锁
    synchronized也能保证内存的可见性
3)可重入锁

不可重入锁:一个线程没有释放锁,在第二次加锁的时候就会阻塞等待,直到第一次的锁被释放,才能获取到锁。可是,如果释放锁和等待获取锁的是同一个线程,那么此时就会陷入死锁的困境,这种情况就是不可重入锁。
synchronized是一个可重入锁,在可重入锁的内部,包含了“线程持有者”和“计数器”两个信息

  • 如果某个线程加锁的时候,发现锁已经被占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并且让计数器自增
  • 解锁的时候计数器递减为0的时候,才真正释放锁。

synchronized的使用

  • 修饰普通方法:锁的对象
public class SynchronizedDemo {
    public synchronized void methond() {
    }
}
  • 修饰静态方法:锁的类
public class SynchronizedDemo {
    public synchronized static void methond() {
    }
}
  • 修饰代码块:明确指定锁的哪个对象
//锁当前对象
public class SynchronizedDemo {
    public void method() {
    	synchronized (this) {
		}
	} 
}
//锁类对象
public class SynchronizedDemo {
    public void method() {
    	synchronized (Synchronized.class) {
		}
	} 
}

volatile关键字

volatile修饰的变量,能够保证“内存可见性”。
加上volatile,会让修饰的变量强制读写内存

wait和notify

  • wait():让当前线程进入等待状态
  • notify()、notifyAll():唤醒在当前对象上等待的线程
wait的工作原理
  • 使当前执行代码的线程进行等待
  • 释放当前的锁
  • 满足一定条件时被唤醒,重新尝试获取这个锁
    wait要搭配synchronized来使用,脱离synchronized使用wait会直接抛出异常
wait结束等待的条件
  • 其他线程调用该对象的notify方法
  • wait等待时间超时
  • 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException异常
public class Locker {
    private static Object locker = new Object();
    static class WaitTask implements Runnable{
        @Override
        public void run() {
            synchronized (locker){
                System.out.println("wait start");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait end");
            }

        }
    }

    static class NotifyTask implements Runnable{
        @Override
        public void run() {
            synchronized (locker){
                Scanner in = new Scanner(System.in);
                System.out.println("notify");
                in.next();
                locker.notify();
                System.out.println("notify end");
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new WaitTask());
        Thread t2 = new Thread(new NotifyTask());
        t1.start();
        t2.start();
    }
}
wait和sleep的对比

wait和sleep是完全没有可比性的,因为一个是用于线程间的通信,而另一个是让线程阻塞一段时间
其相同点 是都可以让线程放弃执行一段时间
其不同点有
1.wait需要搭配synchronized使用,sleep不需要
2.wait是Object的方法,sleep是Thread的方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值