程序猿的博客


  • 首页

  • 标签

  • 分类

  • 归档

线程安全和JMM

发表于 2018-08-06 | 分类于 多线程

多线程存在的问题

多线程运用得好可以大大提高系统的性能。但是使用不当也会对系统造成毁灭性灾难。

  1. 线程安全问题。多个线程操作共享数据时,会产生线程安全问题。导致读取脏数据或者丢失更新等问题
  2. 线程活性问题。由于程序问题导致一个线程一直处于非Runnable状态或者处于Runnable状态但执行的任务没有紧张称为线程活性问题。例如:两个线程,线程1需要先占用锁1,再占用锁2。线程2需要先占用锁2,再占用锁1。这是如果线程1占用了锁1,线程2占用了锁2。他们都占用了对方需要的锁,双方都阻塞等待对方的锁释放,导致死锁。
  3. 上下文切换。线程切换引起的上下文切换,会增加系统消耗。

线程安全问题

线程安全问题是多个线程在操作共享数据引起的。要保证线程安全,就需要保证对共享数据的操作有三个性质:原子性,可见性和有序性。

原子性

原子性是指涉及共享数据的操作对别的线程是不可分割的。即其他线程只能看到该操作未发生或者已经结束。

注意 i++ 并不是原子性操作,i++实际上是一个read-modify-write操作。

  1. 先读取出i的值
  2. 修改i的值
  3. 写回内存
可见性

可见性是指一个线程对共享数据修改后,其他线程可以看到修改后的值。

导致可见性的原因
  1. 由于java内存模型中,每个线程都有一个工作内存。在对共享数据进行修改和读取时,
    是先对工作内存中的数据进行操作。所以其他线程读取的共享变量可能是脏数据,无法保证可见性。
  2. 有序性影响可见性。重排序后,一个线程对共享变量的更新对其他线程来说可能变得不可见。如原顺序,操作1->操作2,操作2读取操作1的结果 。重排序后顺序, 操作2->操作1。操作1的结果对操作2是不可见的。
Java内存模型

java内存模型,简称JMM。java线程之间的通信是通过JMM控制的。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有其工作内存,存有共享变量的副本。线程对共享变量的读写都是先对工作内存进行,工作内存再将共享变量和内存同步。

1
工作内存是抽象概念,并非真实存在。它蕴含了缓存,写缓存区,寄存器还有其他硬件等。

image

有序性

JIT编译器,处理器和存储子系统为了优化系统,会对代码进行重排序。重排序按照as-if-serial语义,保证重排序后在单线程时运行结果是一样的。但是多线程时,无法保证有序性。

代码经过各级重排序优化再最终执行
image

happens-before

happens-before规则是JMM对多线程重排序的约束规则,遵循happens-before规则的重排序不会改变多线程的执行结果。

1
2
3
int a=1; //A
int b=3; //B
int c=a+b; //C

A happens-before B(非必须)

A happens-before C

B happens-before C

JMM对happens-before的定义:

  1. 如果一个操作happens-before另一个操作,那么操作一的执行结果对第二个操作时可见的,并且第一个操作执行顺序在第二个操作之前。
  2. 两个操作如果存在happens-before规则,并不意味者java平台会按照happens-before的执行顺序执行。如果重排序的执行结果和按happens-before顺序执行的结果一致的话,jmm允许这种排序。

定义1是JMM对程序员的保证.程序员关注结果,最终的执行结果和按执行顺序执行的结果一致。

定义2是JMM对编译器和处理器重排序的约束。

两者并不冲突。

下面是happens-before规则:

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
  2. 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
  8. 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

happens-before规则也保证了可见性,先执行的操作结果对后面执行的操作是可见的。

happens-before推导

1
2
3
4
5
6
7
8
9
10
11
private volatile boolean flag;
private int i;
public void read(){
i=1; //1
flag=true; //2
}
public void write(){
if(flag){ // 3
int j=i; //4
}
}

  1. 根据程序规则,1 happens-before 2 ; 3 happens-before 4
  2. 根据volatile规则,2 happens-before 3
  3. 根据传递性规则,由 1 happens-before 2,2 happens-before 3,3 happens-before 4 => 1 happens before 4

image

线程安全解决方案

多线程安全问题是因为多个线程同时操作共享变量,缺乏同步机制来协调线程间数据的访问和活动。

  1. 避免多线程操作共享变量
  2. 利用jdk提供了锁,volatile关键字等线程同步机制

多线程基础

发表于 2018-08-05 | 分类于 多线程

多线程的概念

进程是程序运行的实例。启动一个java程序其实就是启动了一个jvm的进程。进程是程序向操作系统申请资源(内存空间、文件句柄等)的基本单位。

线程是进程中可以独立执行的最小单位。

进程和线程的关系,可以比喻成一个项目组和组员的关系。项目组完成一个项目需要需求,开发,测试。这些往往都是并行的。需要需求,开发和测试人员协作完成。他们共享项目组的资源,如需求文档,功能代码等。

为什么需要线程

  1. 提高cpu的利用率。因为cpu的执行速率是远远高于io操作的。如果单线程运行,遇到io操作,系统会一直等待io完成再往下执行。这段io操作的时间内浪费了cpu的性能。
  2. 提高响应速率。为了用户的体验,在使用GUI软件时,一个慢的IO操作不至于会使软件”冻住”。
  3. 充分利用多核。对应已经普及的多核计算机,多线程可以重复利用其性能。

java中的线程

java中的线程是通过java.lang.Thread类来实现。一个Thread或其子类就是一个线程。

创建线程

创建执行线程的方式有三种:

Thread类的构造函数大概可以分为要Runnable和不需要Runnable两种。 Thread()和 Thread(Runnable)。所以可以根据这两种方式来创建一个线程。

继承Thread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   //1.继承Thread类
class ThreadDemo extends Thread{

//2.重写run方法。
public void run() {
// 业务逻辑
}
}

public static void main(String[] args) {
//new 一个Thread对象
Thread thread = new ThreadDemo();
//调用start方法启动一个线程。线程启动后会执行run方法。
thread.start();
}
实现Runnable接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   //1.实现Runnable类
class RunnableDemo implements Runnable{
//2.实现run方法
public void run() {
// 业务逻辑
}

}

public static void main(String[] args) {
//new 一Runnable实现类
Runnable run = new RunnableDemo();
//通过Runnable构造Thread
Thread thread2 = new Thread(run);
//启动线程
thread2.start();
}
实现callable接口

上面两种线程都没有返回值,jdk1.5后出现了callable能创建具有返回值的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
   //实现Callable接口
class CallableDemo implements Callable<String>{
//实现call方法
public String call() throws Exception {
return "callable";
}

}
public static void main(String[] args) {

//callable有两种使用方式:

//1.通过线程池
//创建线程池
ExecutorService threadService = Executors.newSingleThreadExecutor();
//提交callable任务
Future<String> submit = threadService.submit(new CallableDemo());
//阻塞获取返回值
String object = submit.get();
System.out.println(object);

//2. 通过FutureTask
FutureTask<String> task1 = new FutureTask<String>(new CallableDemo() {
});
Thread thread1 = new Thread(task1);
thread1.start();
System.out.println(task1.get());
}
三种创建线程方式的差异

callable和runnable除了callable可以有返回值,其他的都一样。实际上callable最终会被构造成一个实现Runnable接口的类。例如上面的FutureTask。

主要分析Runnable方式和继承Thread这两种方式。

  1. 继承Thread是通过继承的方式,实现Runnable是一种组合的方式。组合相对于继承更加低耦合和灵活。《Effictive java》中也提到复合优先于继承的观点。

  2. 实现Runnable的方式,容易线程间实现资源的共享。因为一个Runnable对象可以用来创建多个线程。对象的成员变量被多个线程共享。这也使得我们要注意线程安全问题。

线程的属性

属性 作用
编号(ID) 用于标识不同线程
名称(Name) 用于区分不同线程 默认:”thread-0” .可以设置。
线程类别(Daemon) 可以设置守护线程。主线程结束,守护线程也会结束
优先级(Priority) 可以设置线程优先级,1-10.默认5.

线程的状态

image

状态 说明
创建(New) 线程被创建未启动。java中就是还没调用start方法之前
可运行(Runnable) 该状态下有两种情况,一种是获取到cpu的时间片
阻塞(BLOCKED) 发起阻塞IO或者争取独占锁资源。该状态不占用cpu
等待(Waiting) 执行特定的方法会,等待其他线程唤醒
有时限等待(Timed Waiting) 和Waiting状态类似,但是Timed Waiting是有时限的,指定时间到会结束等待状态
消亡(Terminated) run方法执行完毕或者发生异常。
1…45

wujiazhen

42 日志
18 分类
27 标签
GitHub
© 2019 wujiazhen
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4