导读
1. 线程基础概念、线程安全概念、多个线程多个锁概念
2. 对象锁的同步和异步
3. 脏读概念、脏读业务场景
4. Synchronized概念、Synchronized代码块、Synchronized其他细节
要说线程,首先先谈谈进程。在操作系统中,运行中的应用程序称为进程,是操作系统分配和管理资源(内存、CPU等资源)的基本单位,具有完整的虚拟地址空间。
为了描述和控制进程的运行,系统为每个进程定义了一个数据结构,称为进程控制块(PCB);进程控制块记录了操作系统所需的、用于描述进程当前情况以及控制进程运行的全部信息。
进程题外话
操作系统每时每刻运行着许多进程,如何能够保证多个进程能够并发运行和进程运行的正确性以及快速切换进程运行的上下文环境,这一切都是PCB的功劳。
例如:当OS要调度某进程执行时,要从该进程的PCB中查出该进程现行状态和优先级,并根据其PCB中的程序和数据的内存地址,找到其程序和数据;在进程执行过程中,当需要与其他进程合作时实现同步、通信或者访问文件时,也需要访问PCB;当进程由于某种原因而暂停执行,又须将其断点的处理机环境保存在PCB当中。程控制块(PCB)包含的信息:
- 进程标示符:唯一标识一个进程,通常有2中:内部标识符和外部标识符
- 处理机状态:处理机中各种寄存器的内容,包括通用寄存器、指令寄存器、程序状态字、用户栈指针
- 进程调度信息:进程状态,进程优先级,事件,调度算法等等
- 进程控制信息:程序和数据的地址,进程同步和通信机制,资源清单,链接指针
进程间的通信方式:
- 管道(pipe),有名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信。有名管道允许非亲缘关系的进程进行通信
- 信号(signal):信号是在软件层面上对中断机制的一种模拟,用于通知进程有某事件发生,一个进程收到一个信号和处理机收到一个中断请求效果上可以说是一样的。
- 消息队列(message queue):消息队列是消息的链接表,具有写权限的进程往队列中添加新消息,而具有读权限的进程从消息队列中取消息。达到通信的目的
- 共享内存(shared memory):多个进程可以访问同一内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种需要依赖某种同步机制,如互斥锁和信号量
- 信号量(semaphore):主要作为进程与进程之间,同一进程的不同线程之间的同步和互斥手段
- 套接字(socket):用于网络不同机器的进程之间的通信
线程为轻量级组件,操作系统中能够独立调度和运行的基本单位。是进程中执行运算的最小单位,也就是处理机调度的基本单位。它比进程更小的,能够独立运行的基本单位。
如果把进程理解为逻辑上操作系统的所完成的所有任务,那么线程就表示完成该任务的许多可能的子任务。在进程中的线程,共享进程中的内存空间和其他系统资源,使得系统对于创建线程比进程的开销小,因此线程更加轻量
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程拥有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程只是一个进程中的不同执行路径。线程拥有自己的堆栈(也就是栈)和局部变量,但线程没有单独的地址空间,一个线程死掉等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但进程的切换耗费的资源较大,效率要差。
如果你的代码所在的进程中有多个线程同时运行,而且这些线程可能会同时同一段代码。如果每次运行的结果和单线程运行的结果是一样的,而且其他的变量的值也和预期一样,就是线程安全的。
或则说,对于一个类,当多个线程访问这个类,每次访问的结果和单线程运行的结果是在预期当中,也就是每次运行结果是确定的,可以预知的。那么说这个类是线程安全的。
当一个类做多线程环境中,也能够表现出正确的行为,那么这个类就是线程安全的
线程安全问题都是由全局变量和静态变量引起的,也就是多个线程不预期地对共享变量进行写操作,这样就会造成线程安全的问题。
许多个线程同时访问了变量inc,这时对于变量inc来说该变量就是共享变量。此时就会存在线程安全的问题。
如果想要保证线程安全,则需要在increase方法中加上synchronized关键字,对方法进行同步,一个时间内只允许一个线程访问。
public class Test {
public int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
首先要明白,什么是对象锁。就是对象监视器(Monitor),Java中Jvm会对每个创建的对象都会分配一个监视器,这个监视器原语用于中多线程并发环境中,用于保证线程的有序执行,一个时刻只允许单一线程进入。这就是所说的对象锁,使用synchronized关键字可以使一个时刻只允许一个线程进入代码块。
也就是说,当有多个对象分别中自己的线程中执行时,每个线程都持有对应的对象的锁,这些锁互不干扰,也不会互斥,因为锁是不同的,每个对象都持有自己的锁。
在下面的案例中,没有加synchronized同步。如果中increase方法中加上synchronized关键字,对于该案例来说就是多个线程一个锁,这时当后面的线程想要执行increase方法,必须得等到前面的线程执行完之后才能执行,也就是一个时间内只允许一个线程访问。
public class Test {
public int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
类似于集合Vector对象,如果创建了2个vector对象,在两个线程中使用。这是两个线程分别持有不同锁。这就是对个多个对象多个锁模型。
当中对象实例的方法上加synchronized关键字时称为对象同步,否则为异步。
在异步中,不同线程访问互不影响。同步则必须等待前面的线程执行之后,后面的线程才能进入方法执行。
脏读是指多个线程对数据读或者写的操作,读线程读取到的数据不是预期的,这种就会称为线程脏读。
线程T1对同一个A进行写的操作,其中线程T2负责读。当写线程T1读取A的值并做修改,这时候还没写到主存当中,CPU被切换了,这时读线程T2取A的值,拿到的是A旧值。这时CPU又切换回T1,把新的值写回主存当中。此时线程T1读取的值与最新的值不是同一个,这时就会称为线程脏读。
/**
* Created by Gavin on 2016/10/15.
*
* 线程脏读
* 脏读是指多个线程对数据读或者写的操作,读线程读取到的数据不是预期的,这种就会称为线程脏读。
*/
public class DirtyRead {
public static void main(String[] args) throws InterruptedException {
// 操作类对象引用
final UserService userService = new UserService();
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("对象更新前:" + userService.getUser());
System.out.println("对象正在更新");
userService.updateUser("莉丝", 50);
System.out.println("对象更新完毕");
System.out.println("更新后对象:" + userService.getUser());
}
});
t.start();
// 为了体现脏读效果明显,休眠100毫秒后启动线程2
Thread.sleep(100);
Thread t2= new Thread(new Runnable() {
public void run() {
System.out.println("获取对象:" + userService.getUser());
}
});
t2.start();
}
}
class UserService {
private UserInfo userInfo = new UserInfo("张三", 24);
public void updateUser(String name, int age) {
userInfo.setAge(age);
try {
// 休眠,模拟复杂的业务操作,耗时
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
userInfo.setUsername(name);
}
public UserInfo getUser() {
return userInfo;
}
}
/**
* 用户信息实体
*/
class UserInfo {
private String username;
private int age;
public UserInfo(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "UserInfo{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
}
运行结果:
对象更新前:UserInfo{username='张三', age=24}
对象正在更新
获取对象:UserInfo{username='张三', age=50}
对象更新完毕
更新后对象:UserInfo{username='莉丝', age=50}
“张三”原来是24岁,当通过getuser获取用户信息时,“张三”变成了50岁。这就是脏读,因为另外一个线程要把“张三”的信息改成“莉丝”50岁,线程还未执行完毕,另外一个线程去获取信息时,出现的信息错乱。
在编写一个类时,如果该类代码可能运行于多线程环境下,那么就需要考虑同步的问题了。在Java中内置了语言级的同步原语synchronized,使得在同一时间内只有一个线程可以进行操作,而且被synchronized同步的代码块是原子操作。
Synchronized可以在声明在方法当中,也可以使用在方法里面的某个代码块当中,亦可以在静态方法中使用,synchronized在不同的代码块当中所采用的锁会有所不同。如果synchronized声明在方法当中,则监视器为该方法所属的对象;如果在同步代码块当中可以自定义监视器对象,但保证多个线程访问时监视器对象是同一个;当在静态方法中时,则监视器对象为该类实例