多线程基础(五)

一、NEW 、 RUNNABLE 、 TERMINATED 状态的转换

使用isAlive()方法验证

复制代码
public class demo7 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            for (int i = 0; i < 1000_000; i++) {

            }
        },"林夕");
        System.out.println(t.getName()+"线程未运行之前"+t.getState());
        t.start();
        while (t.isAlive()){
            System.out.println(t.getName()+"线程正在运行"+t.getState());
        }
        System.out.println(t.getName()+"线程运行结束"+t.getState());
    }
}

二、WAITING 、 BLOCKED 、 TIMED_WAITING 状态的转换

复制代码
public class demo8 {
    public static void main(String[] args) {
        final Object object=new Object();
        Thread t1=new Thread(()->{
         synchronized (object){
             while(true){
                 try {
                     Thread.sleep(1000);
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
             }
         }
        },"t1");
        t1.start();
        Thread t2=new Thread(()->{
           synchronized (object){
               while (true){
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
               }
           }
        },"t2");
        t2.start();
    }

}

使用 jconsole 可以看到 t1 的状态是 TIMED_WAITING , t2 的状态是 BLOCKED

复制代码
public class demo9 {
    public static void main(String[] args) {
        final Object object=new Object();
        Thread t1=new Thread(()->{
            synchronized (object){
                while(true){
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"t1");
        t1.start();
        Thread t2=new Thread(()->{
            synchronized (object){
                while (true){
                    try {
                       object.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"t2");
        t2.start();
    }

}

使用 jconsole 可以看到 t1 的状态是 WAITING

  • BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

三、yield(),可以让出cpu的调度

复制代码
    public static void main(String[] args) {
        final Object object=new Object();
        Thread t1=new Thread(()->{

                while(true){
                    System.out.println("李白");

                }

        },"t1");

        Thread t2=new Thread(()->{

                while (true){
                    System.out.println("杜甫");
                    Thread.yield();
                }
        },"t2");
        t1.start();
        t2.start();
    }

不使用yield的时候李白和杜甫大概是五五开,使用yield的时候杜甫出现的数量会明显超出李白的数量

四、线程安全

某个代码在多线程环境下会出现bug

根本原因是:

  • 线程在操作系统中,随机调度,抢占式运行。
  • 多个线程,同时修改同一个变量(读取是没问题的),如果是一个线程就没有问题
  • 修改操作,不是"原子"的。

不可分割的最小单位;

cpu的视角:一条指令,就是cpu上不可分割的最小单位

cpu在执行调度切换线程的时候,势必会确保执行完毕一条完整的指令(原子)

原子性:我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进 不来了。这样就保证了这段代码的原子性了。

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

复制代码
public class demo11 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50000; i++) {
                    count++;
                }
            }
        });
t1.start();
t2.start();
t1.join();
t2.join();
        System.out.println(count);
    }
}

正常来说count的运行结果应该是100000

但实际的运行结果却未达到这个值,这个是因为++在cpu指令中不是"原子",而是多步操作。

count++背后的三个指令分别是

  • load
  • add
  • save

这三个指令可能在运行的时候进行来回穿插,导致一次++的不完整性。

Java中提供了synchronized()关键字来完成枷锁操作

synchronized()是关键字不是函数,()中需要的是锁对象

这个对象可以是任何对象

当多个线程对同一个对象上锁时,其他的线程就会进入锁阻塞。

但如果只对一个上锁另一个线程还是会执行失败

锁对象的作用就是来区分两个线程,多个线程是否针对同一个对象枷锁

是针对同一个对象枷锁,此时候就会出现"阻塞"

不是针对同一个对象枷锁,此时不会出现阻塞,两个线程仍是随即调度的并发执行

复制代码
public class demo11 {
    public static int count=0;
    public static void main(String[] args) throws InterruptedException {

        Object lock=new Object();
        Object lock2=new Object();
        Thread t1=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 50000; i++) {
                        count++;
                    }
                }
            }
        });
        Thread t2=new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    for (int i = 0; i < 50000; i++) {
                        count++;
                    }
                }

            }
        });
t1.start();
t2.start();
t1.join();
t2.join();
        System.out.println(count);
    }
}

五、synchronized关键字

1.synchronized的特性

1.1互斥:

synchronized会起到互斥的作用,当两个线程执行到同一个对象的时候,后执行到的线程就会进入阻塞等待,等到另一个线程执行完毕之后这个线程才会继续执行。

  • 进入synchronized修饰的代码块,相当于加锁
  • 退出synchronized修饰的代码块,相当于解锁

synchronized用的锁是存在于Java对象里的

一个线程先上了锁,其他线程只能等待这个线程释放,

这个就像卫生间只有一个,陷进去的人锁了门,其他人再着急也只能排队。

1.2、刷新内存

synchronized的工作过程:

  1. 获得互斥锁
  2. 从主内存靠北变量的最新副本到工作的内存
  3. 执行代码
  4. 将更新后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性.

1.3、可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

把自己锁死:

一个线程没有释放锁,然后又尝试再次加锁

复制代码
//第一次枷锁成功
lock();
//第二次枷锁失败进入阻塞等待。
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第 二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无 法进行解锁操作. 这时候就会 死锁

这样的锁成为不可重入锁

代码示例:

复制代码
static class Counter {

   public int count = 0;
   synchronized void increase() {
count++;
    }

  synchronized void increase2() {
  increase();
  }    
}

在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.

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

2、synchronized使用示例

2.1直接修饰普通方法:锁的synchronizedDemo对象

复制代码
public class synchronizedDemo{
public synchronized void methond(){

   }
}

2.2修饰静态方法:锁的synchronizedDemo类的对象

复制代码
public class synchronizedDemo{
public synchronized void static methond(){

   }
}

2.3修饰代码块:明确指定锁哪一个对象

锁当前对象

复制代码
public class synchronizedDemo{
public void methond(){
synchronized (this){
}
   }
}

锁类对象

复制代码
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
        }
    }
}

3Java标准库中的线程安全

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

但是还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

注:StringBuffer 的核心方法都带有synchronized .

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的。例如 String

相关推荐
波诺波2 小时前
项目pid-control-simulation-main 中的 main.py 代码讲解
开发语言·python
我叫蒙奇2 小时前
husky 和 lint-staged
前端
Zzxy2 小时前
HikariCP连接池
java·数据库
我带你来这儿就是为了告诉你我2 小时前
C++23新特性前瞻
开发语言·c++·算法
kyriewen2 小时前
JavaScript 继承的七种姿势:从“原型链”到“class”的进化史
前端·javascript·ecmascript 6
穷鱼子酱2 小时前
ElSelect二次封装组件-实现分页(下拉加载、缓存)、回显
前端
罗超驿2 小时前
Java数据结构_栈_算法题
java·数据结构·
科科睡不着2 小时前
拆解iOS实况照片📷 - 附React web实现
前端
前端老兵AI2 小时前
Electron 桌面应用开发入门:前端工程师的跨平台利器
前端·electron