线程定义
线程,是程序执行流的最小单位。是进程中的一个实体,是被系统独立调用和分派的基本单元,线程自己不拥有系统资源,只是拥有在运行中必不可少的资源,但是可以与同属一个进程的其他线程共享全部资源,一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以兵法执行。单个程序中同时运行多个线程完成不同工作,称之为多线程。
java中并发包:java.util.concurrent
(JUC)
##1. volatile关键字
作用:
- 线程可见性,但不保证操作原子性
- 防止指令重排
volatile修饰的变量/对象,每次修改后会cpu会通知其他线程,使得其他线程使用该变量/对象时从主内存中获取。
volatile不保证原子性,示例:
例子1:不使用volatile关键字
package com.multithread;
import java.util.concurrent.TimeUnit;
public class VolatileTest {
public static int count = 0;
public static void inc() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileTest.inc();
}
}).start();
}
System.out.println("count = " + VolatileTest.count);
}
}
运行结果:count = 963,与期望值1000不同。
例子2:使用volatile关键字
package com.multithread;
import java.util.concurrent.TimeUnit;
public class VolatileTest {
public volatile static int count = 0;
public static void inc() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
VolatileTest.inc();
}
}).start();
}
System.out.println("count = " + VolatileTest.count);
}
}
运行结果:count = 980,与期望值1000不同。
1.1 分析原因
JVM在运行时对内存进行分配,其中一个内存区域是JVM虚拟机栈,每个线程在运行时都会有一个线程栈(TLAB–线程本地分配缓冲),其中保存了线程运行时变量值信息。当线程访问某一个对象的值的时候,首先通过对象引用找到对应堆内存的变量的值,然后把堆内存变量的具体值load到线程内存中,建立一个变量的副本,之后线程就不在和对象在堆内存变量值有任何关系,而是直接修改变量副本的值,在线程退出之前会将修改后副本的值会写到对象的堆内存中的值,这样在堆中对象的变量的值就发生变化了。
下图描述了这种交互:
说明:
- read and load 从主存复制变量到当前工作内存
- use and assign 执行代码,改变共享变量值
- store and write 用工作内存数据刷新主存相关内容
其中 use and assign 可以多次出现,但是这一些操作并不是原子性,也就是在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6,线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。
2. 关键字synchronized
2.1 Java语言的关键字,同步锁。
synchronized修饰的对象有以下几种:
- 类,作用范围是synchronized后面括号括起来的部分,作用的对象是类的所有对象;
- 静态方法,作用的范围是整个静态方法,作用的对象是类的所有对象;
- 方法,称为同步方法,作用的范围是整个方法,作用的对象是调用这个方法的对象;
- 代码块,称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。
作用:
- 多线程的可见性,保证操作原子性
- 防止指令重排
使用的锁:
- 无锁 – new 出来的对象,没有任何同步操作
- 偏向锁 – 对象上有一把锁,该对象就表现为偏向这个线程来获取锁
- 轻量锁 – 对象上有多个锁竞争,就会升级为轻量锁
- 重量锁 – 对象上有很多个锁竞争,而且是在高并发情况下,就会升级为重量级锁,即向cpu申请锁
锁标识:
- 锁的标识是表现在Java 对象head的Mark Word中,使用后2-3位来标识对象的当前锁种类
2.2 例子
2.2.1 修饰类
用法如下:
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo work
}
}
}
示例:
public class SynClass implements Runnable {
private static int count;
public SynClass(int count) {
this.count = count;
}
@Override
public void run() {
synchronized (SynClass.class) {
try {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
调试代码:
SynClass synClassA = new SynClass(0);
SynClass synClassB = new SynClass(0);
new Thread(synClassA).start();
new Thread(synClassB).start();
调试结果:
ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9
说明:synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。
2.2.2 修饰静态方法
用法如下:
public static synchronized void method() {
// todo work
}
示例:
public class SynStaticMethod implements Runnable{
private static int count;
public SynStaticMethod() {
count = 0;
}
public static synchronized void method() {
try {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
TimeUnit.SECONDS.sleep(1);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public synchronized void run() {
method();
}
}
调试代码:
SynStaticMethod synStaticMethodA = new SynStaticMethod();
SynStaticMethod synStaticMethodB = new SynStaticMethod();
new Thread(synStaticMethodA).start();
new Thread(synStaticMethodB).start();
调试结果:
ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9
说明:静态方法是属于类的而不属于对象的。所以synchronized修饰的静态方法锁定的是这个类的所有对象,与类锁是一样的。
2.2.3 修饰非静态方法
synchronized修饰一个方法很简单,就是在方法的前面加synchronized, synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个方法。
用法如下:
public synchronized void method() {
// todo work
}
示例:
定义一个类用来同步方法
public class NotStaticMethodService {
private static int count;
public NotStaticMethodService() {
this.count = 0;
}
public synchronized void synMethod() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
定义线程类
public class SynNotStaticMethod implements Runnable {
private NotStaticMethodService notStaticMethodService;
public SynNotStaticMethod(NotStaticMethodService notStaticMethodService) {
this.notStaticMethodService = notStaticMethodService;
}
@Override
public void run() {
notStaticMethodService.synMethod();
}
}
调试代码:
NotStaticMethodService notStaticMethodService = new NotStaticMethodService();
SynNotStaticMethod synNotStaticMethodA = new SynNotStaticMethod(notStaticMethodService);
SynNotStaticMethod synNotStaticMethodB = new SynNotStaticMethod(notStaticMethodService);
new Thread(synNotStaticMethodA).start();
new Thread(synNotStaticMethodB).start();
调试结果:
ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9
2.2.4 代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
示例:
public class SynBlockRunnable implements Runnable {
private static int count;
public SynBlockRunnable() {
count = 0;
}
@Override
public void run() {
synchronized (this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadName:" + Thread.currentThread().getName() + ", count = " + count++);
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
调试代码:
SynBlockRunnable synBlockRunnable = new SynBlockRunnable();
new Thread(synBlockRunnable).start();
new Thread(synBlockRunnable).start();
调试结果:
ThreadName:Thread-0, count = 0
ThreadName:Thread-0, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-0, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-1, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-1, count = 8
ThreadName:Thread-1, count = 9
说明:
当两个并发线程(thread1和thread2)访问同一个对象(SynBlockRunnable)中的synchronized代码块时,同一时刻只能有一个线程得到执行,另一个线程则受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
将上面调试代码修改如下:
SynBlockRunnable synBlockRunnableA = new SynBlockRunnable();
SynBlockRunnable synBlockRunnableB = new SynBlockRunnable();
new Thread(synBlockRunnableA).start();
new Thread(synBlockRunnableB).start();
此时调试结果为:
ThreadName:Thread-0, count = 0
ThreadName:Thread-1, count = 1
ThreadName:Thread-0, count = 2
ThreadName:Thread-1, count = 2
ThreadName:Thread-0, count = 3
ThreadName:Thread-1, count = 4
ThreadName:Thread-1, count = 5
ThreadName:Thread-0, count = 6
ThreadName:Thread-1, count = 7
ThreadName:Thread-0, count = 8
说明:
这时创建了两个SynBlockRunnable的对象synBlockRunnableA和synBlockRunnableB,线程Thread1执行的是synBlockRunnableA对象中的synchronized代码(run),而线程Thread2执行的是synBlockRunnableB对象中的synchronized代码(run);但synchronized锁定的是对象,这时会有两把锁分别锁定synBlockRunnableA对象和synBlockRunnableB对象,这两把锁是互不干扰的,不形成互斥,所以两个线程可以同时执行。
2.3 字节码实现
可通过javap -c命令来生成类的字节码中包含 monitorenter和monitorexit指令。synchronized关键字是基于这两个指令来实现锁的获取和释放过程。
2.4 Synchronized 的实现过程
- Java代码: synchronized 关键字
- 字节码:monitorenter monitorexit
- 锁升级:执行过程中自动升级
- 操作系统,cpu汇编指令:lock comxchg