public class Demo1 {
public static void main(String[] args) {
Thread thread = new Thread(() ->{
});
//通过thread.getState()方法来查看当前线程的状态
System.out.println(thread.getState());
thread.start();
}
}
//结果:
NEW
public class Demo {
public static void main(String[] args) {
Thread thread = new Thread(() ->{
//这里要让这个线程处于就绪态, 这个线程不能执行任何指令
});
thread.start();
System.out.println(thread.getState());
}
}
//结果:RUNNABLE
注意体会这两段代码的不同.
public class Demo {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(() ->{
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
Thread.sleep(1000);
System.out.println(thread.getState());
}
}
//结果:
TIMED_WAITING
操作系统在进行线程调度的时候, 调度的顺序是随机的(抢占式执行), 正因为这样的随机性, 就可能导致程序的执行出现问题, 如果多线程下程序运行的结果是符合我们预期, 也就是和在单线程中的运行结果是相同的, 我们就说这个程序是线程安全的, 否则, 这个程序就是线程不安全的.
我们操作两个线程, 分别对同一个数字自增5_0000次,查看运行结果:
class Counter{
public int count;
public void increase(){
count++;
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
Thread thread1 = new Thread(() ->{
for(int i=0;i<5_0000;++i){
counter.increase();
}
});
Thread thread2 = new Thread(() ->{
for(int i=0;i<5_0000;++i){
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
//多次运行查看运行结果:
65130
57228
54039
74729
在上述代码中, 我们一共对count执行了10_0000次, 但结果却小了很多, 并且每次执行结果不一样, 这个程序如果在单线程中执行, 结果一定是等于10_0000的, 这时, 多线程执行的结果与单线程不相等, 这个程序就是线程不安全的.
Q:如何解决这个问题?
正确的做法是:加锁. 在thread1对count进行操作前, 为thread1加锁(lock), 此时lock就会一直处于阻塞状态, 操作完成后再解锁(unlock), 这样就能将一个乱序的并发转变为串行操作, 此时便能得到正确的结果.
注意:线程的并发性越高, 速度越快, 但同时可能就会出现一些问题, 加了锁之后, 线程间的并发性降低, 速度降低, 此时得到的数据也会更准确.
那么, 加锁之后的并发执行不就和串行一样了吗?这样执行有什么意义呢?
在实际开发中, 一个线程可能会执行多个任务, 例如, 线程一可能要执行步骤1, 2, 3, 4, 而执行过程中只有步骤4需要加锁,那么对于步骤1, 2, 3, 依然可以并发执行.
Java中加锁的方式有很多种, 其中最常用的就是synchronized这个关键字.
class Counter{
public int count;
//通过synchronized关键字对自增操作加锁
synchronized public void increase(){
count++;
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException{
Counter counter = new Counter();
Thread thread1 = new Thread(() ->{
for(int i=0;i<5_0000;++i){
counter.increase();
}
});
Thread thread2 = new Thread(() ->{
for(int i=0;i<5_0000;++i){
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
//结果:
100000
给方法加上synchronized关键字后, 此时运行此方法, 就会自动加锁. 结束这个方法, 就会自动解锁. 当一个线程对这个方法进行加锁之后, 另一个线程尝试加锁时就会触发阻塞等待(此时对应的线程就处在blocked状态), 阻塞状态会持续到占用锁的线程将锁释放为止.
- 线程是抢占式执行, 线程间的调度充满随机性. //这种操作是线程不安全的根本原因, 但无法避免
- 多个线程对同一个变量进行修改操作 //部分情况下可以通过修改代码来避免
- 针对变量的操作不具有原子性(这些操作是可以拆分的) //加锁操作就是将多条指令打包, 使其具有原子性
- 内存可见性影响到线程安全 //编译器优化的影响
- 指令重排序 //同样是编译器优化的影响, 编译器会智能地取调整代码的执行顺序, 从而提高代码的执行效率
例如, 线程A在持续地读取某一个数, 此时, 线程B将这个数字进行了修改, 但线程A读取到的依然是未被修改的值.
public class Demo {
static int flg = 0;
public static void main(String[] args) {
Thread thread = new Thread(() ->{
while (flg==0){
}
System.out.println("循环退出, thread线程执行完毕");
});
thread.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入flg的值:");
flg = scanner.nextInt();
System.out.println("main线程执行结束");
}
}
//结果:
请输入flg的值:
5
main线程执行结束
//程序并没有退出, 因为thread线程并没有执行完毕
在上述代码中, thread线程开始执行后开始持续读取flg的值, 此时我们在main线程中修改了flg的值, 但thread线程并没有因为flg的修改而结束, 这是由于thread线程持续从内存中读取到了相同的数据, 但从内存中读取数据效率很低, Java编译器就对这个操作进行了优化, 使得thread线程直接从寄存器中读取数据, 但main线程是在内存中修改了flg的值, 此时thread线程无法感知到, 自然就导致了线程无法正常结束.
读取内存中的数据是一个非常低效的操作(相比于读取寄存器中的数据来说). 在某个线程持续读取内存中的数据时, Java编译器就会对这个操作产生优化, 让线程不从内存中, 而是直接从寄存器中读取数据, 加快了执行效率(编译器会在编译过程中对代码进行调整, 在不改变代码逻辑的前提下, 加快程序的执行效率), 这种优化一般情况下不会出现问题, 但在多线程下, 这里的优化可能会造成误判, 这也就是内存可见性造成的影响.
解决方案:
对于上述问题, 使用volatile关键字解决:
public class Demo {
//使用volatile关键字修饰flg
static volatile int flg = 0;
public static void main(String[] args) {
Thread thread = new Thread(() ->{
while (flg==0){
}
System.out.println("循环退出, 执行完毕");
});
thread.start();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入flg的值:");
flg = scanner.nextInt();
System.out.println("main线程执行结束");
}
}
//结果:
请输入flg的值:
1
main线程执行结束
循环退出, 执行完毕
//程序成功退出
synchronized关键字互斥性:
synchronized关键字会达到互斥的效果, 某个对象中的某个方法使用了synchronized关键字, 当一个线程正在执行这个对象的这个加锁的方法时, 如果其他线程也执行到了这个方法, 就会阻塞等待(注意:必须是多个线程同时执行到同一个对象的同一个加锁的方法).
synchronized关键字的用法
public class Demo{
public synchronized void func(){
System.out.println("一个加锁的普通方法");
}
}
public class Demo{
public static synchronized void func1(){
System.out.println("一个加锁的静态方法");
}
}
public class Demo{
{
synchronized(this){
System.out.println("一个加锁的代码块");
}
}
}
注意:修饰代码块时, 我们需要显式指定需要加锁的对象(Java中任何一个对象都可以作为锁对象)
The end