Java之线程篇三

新绿MEHO 2024-09-10 14:35:01 阅读 81

​​​​​​​

目录

线程状态

观察线程的所有状态

线程状态及其描述

线程状态转换

代码示例1

代码示例2

线程安全 

概念

线程不安全的代码示例

线程不安全的原因

线程安全的代码示例-加锁

synchronized关键字

synchronized的特性

小结

形成死锁的四个必要条件

synchronized的使用示例

Java标准库中的线程安全类


线程状态
观察线程的所有状态

线程的状态是一个枚举类型 Thread.State

<code>public class Demo11 {

public static void main(String[] args) {

for(Thread.State state:Thread.State.values())

System.out.println(state);

}

}

运行结果

线程状态及其描述

NEW:Thread对象已经有了,但是start方法还没调用;

RUNNABLE:就绪状态,线程已经在CPU上执行了/线程正在排队等待执行(即工作中或即将开始工作);

TERMINATED:Thread对象还在,但是内核中的下线程已经没了,即工作完成了;

TIMED_WARTING:阻塞状态,由于sleep这种固定时间的方式产生的阻塞;

WAITING:阻塞,由于wait这种不固定时间的方式产生的阻塞;

BLOCKED:阻塞,由于锁竞争导致的阻塞。

线程状态转换

代码示例1

<code>public class Demo11 {

public static void main(String[] args) {

Object object=new Object();

Thread t1=new Thread(new Runnable() {

@Override

public void run() {

synchronized (object){

while(true){

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

},"t1");

t1.start();

Thread t2=new Thread(new Runnable() {

@Override

public void run() {

synchronized (object){

System.out.println("hello");

}

}

},"t2");

t2.start();

}

}

通过jconsole可以看到t1的状态是TIMED_WAITING,t2的状态是BLOCKED。

代码示例2

<code>public class Demo11 {

public static void main(String[] args) {

Object object=new Object();

Thread t1=new Thread(new Runnable() {

@Override

public void run() {

synchronized (object){

while(true){

try {

object.wait();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

},"t1");

t1.start();

}

}

通过jconsole可以看到t1的状态是WAITING.

小结

BLOCKED表示等待获取锁,WAITING和TIMED_WAITING表示等待其它线程发来通知;

TIMED_WAITING线程在等待唤醒,但设置了时限;

WAITING线程在无限等待唤醒。

线程安全 
概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全的代码示例

<code>public class Demo12 {

private static int count=0;

public static void main(String[] args) throws InterruptedException {

Thread t1=new Thread(()->{

for (int i = 0; i < 50000; i++) {

count++;

}

});

Thread t2=new Thread(()->{

for (int i = 0; i < 50000; i++) {

count++;

}

});

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println("count: "+count);

}

}

运行结果

结果与预期结果不一致且差别很大,显然上述代码是线程不安全的。

线程不安全的原因

1.修改共享数据

上述代码涉及到多线程(两个及两个以上的线程)针对同一个变量count进行修改。

2.原子性

一条Java语句不一定是原子的,也不一定只是一条指令。

比如count++,其实是由三步操作组成的:

1.从内存中把数据读取到CPU;

2.对变量count进行++;

3.把数据写回到内存。

如果一个线程正在对一个变量操作,中途其它线程插入进来了,如果这个操作被打断,结果就可能是错误的。

3.可见性

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

Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型. 

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.

线程之间的共享变量存在 主内存 (Main Memory). 

每一个线程都有自己的 "工作内存" (Working Memory) . 

当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据. 

当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存. 

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.

4.指令重排序

如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,原来是按1->2->3的方式执行,优化后可能会按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序。

编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 但

是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代

码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

线程安全的代码示例-加锁

<code>public class Demo12 {

private static int count=0;

public static void main(String[] args) throws InterruptedException {

Object locker=new Object();

Thread t1=new Thread(()->{

for (int i = 0; i < 50000; i++) {

synchronized (locker) {

count++;

}

}

});

Thread t2=new Thread(()->{

for (int i = 0; i < 50000; i++) {

synchronized (locker) {

count++;

}

}

});

t1.start();

t2.start();

t1.join();

t2.join();

System.out.println("count: "+count);

}

}

运行结果

synchronized关键字
synchronized的特性

1)互斥

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

进入 synchronized 修饰的代码块, 相当于 加锁。

退出 synchronized 修饰的代码块, 相当于 解锁。

synchronized用的锁是存在Java对象头里的。

synchronized

的底层是使用操作系统的

mutex lock

实现的

.

2)可重入

Java

中的

synchronized

可重入锁

,

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

理解死锁

一个线程没有释放锁

,

然后又尝试再次加锁

.

<code>// 第一次加锁, 加锁成功

lock();

// 第二次加锁, 锁已经被占用, 阻塞等待.

lock();

按照之前对于锁的设定

,

第二次加锁的时候

,

就会阻塞等待

.

直到第一次的锁被释放

,

才能获取到第

二个锁

.

但是释放第一个锁也是由该线程来完成

,

结果这个线程已经躺平了

,

啥都不想干了

,

也就无

法进行解锁操作

.

这时候就会

死锁

.

代码示例

static class Counter {

   public int count = 0;

   synchronized void increase() {

       count++;

  }

   synchronized void increase2() {

       increase();

  }

}

在上面的代码中, increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的. 

在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释

放, 相当于连续加两次锁),这个代码是完全没问题的. 因为 synchronized 是可重入锁.

小结

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

如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增. 

解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

形成死锁的四个必要条件

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

1.互斥条件(锁的基本特性)

   当一个线程持有一把锁之后,另一个线程也想要获取到锁,就要阻塞等待。

2.不可抢占条件(锁的基本特性)

   当锁已经被线程1拿到之后,线程2只能等线程1主动释放,不能强行抢过来。

3.请求与保持条件(代码结构)

   一个线程尝试获取多把锁,已经获取到部分数量的锁,但仍尝试获取其它线程已经占有的锁。

4.循环等待/环路等待(代码结构)

   等待的依赖关系,形成了环。

这四个条件同时满足时,系统中就可能发生死锁。

解决死锁的方法通常包括死锁预防、死锁避免、死锁检测和死锁恢复等策略。 

比如包括调整代码结构,避免循环等待;对锁进行编号,先加编号大的锁或编号小的锁。

synchronized的使用示例

 synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

1.直接修饰普通方法

public class SynchronizedDemo {

   public synchronized void methond() {

  }

}

2.直接修饰静态方法

public class SynchronizedDemo {

   public synchronized static void method() {

  }

}

3.修饰代码块

锁当前对象

public class SynchronizedDemo {

   public void method() {

       synchronized (this) {

           

      }

  }

}

锁类对象

public class SynchronizedDemo {

   public void method() {

       synchronized (SynchronizedDemo.class) {

      }

  }

}

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待. 

两个线程分别尝试获取两把不同的锁, 不会产生竞争.

Java标准库中的线程安全类

Java

标准库中很多都是线程不安全的

.

这些类可能会涉及到多线程修改共享数据

,

又没有任何加锁措施

.

ArrayList

LinkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

但是还有一些是线程安全的

.

使用了一些锁机制来控制

.

Vector (

不推荐使用

)

HashTable (

不推荐使用

)

ConcurrentHashMap

StringBuffer

我们可以看到,例如StringBuffer类的成员,有不少是加锁的:



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。