(一)synchronized
线程开始运行,拥有自己的栈空间,就如同一个脚本一样,按照既定的代码一步一步地执行,直到终止。但是,每个运行中的线程,如果仅仅是孤立地运行,那么没有一点儿价值,或者说价值很少,如果多个线程能够相互配合完成工作,包括数据之间的共享,协同处理事情。这将会带来巨大的价值。
Java支持多个线程同时访问一个对象或者对象的成员变量,关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
1.1 对象锁
对象锁也就是实例锁
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 10000; i++) {
count++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
1.2 类锁
类锁其实是概念上的一种,并不是真实的一种,类锁其实锁的是类对应的class对象,锁和对象锁之间也是互不干扰的。
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
setCount();
});
Thread thread2 = new Thread(() -> {
setCount();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
private synchronized static void setCount() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
1.3 锁失效场景
- 多个线程,其中一个线程没有对资源加锁。
static int count = 0;
public static void main(String[] args) throws InterruptedException {
//thread1不使用锁
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count++;
}
});
//thread2使用类锁
Thread thread2 = new Thread(() -> {
setCount();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
private synchronized static void setCount() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
- 多个线程,使用的锁不是同一把锁。
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
//thread1使用对象锁
Thread thread1 = new Thread(() -> {
synchronized (lock) {
for (int i = 0; i < 1000; i++) {
count++;
}
}
});
//thread2使用类锁
Thread thread2 = new Thread(() -> {
setCount();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
private synchronized static void setCount() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
1.4 线程安全问题
模拟重复出票问题,未添加锁
static int count = 10;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
setCount();
});
Thread t2 = new Thread(() -> {
setCount();
});
t1.start();
t2.start();
}
private static void setCount() {
while (count > 0 ){
try {
Thread.sleep(50); //模拟业务耗时
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() +"线程获取到了第" + count -- + "张票");
}
}
(二)volatile
使用于一个线程写,多个线程读的场景,它能够保证每次读取都能够是最新的值,但是不能够保证读取到最新的值使用的过程中不被修改。很可能在使用之后,最新值已经变更。原数据变成过期数据,这时候就会出现数据不一致(非同步)的问题。
排他性:
同一个时间节点,只能有唯一的一个线程能够得到锁,执行程序逻辑,其他线程必须等待该线程的锁释放
可见性
同一个时间节点,允许有多个线程操作共享变量。
线程被调度时,变量的的值会被加载到CPU的缓存中,等到执行完毕之后再重新写入到主内存中,再此期间其他线程修改了变量的值这个线程是不知道的。
如果该变量添加了volatile
关键字,一个线程修改了变量的值,其他线程在执行的过程中获取该变量的值是最新的。
但是这并不意味着它是线程安全的,因为缺少排他性即便获取到最新的值,其他线程也能对变量进行操作,这就是为什么volatile并不能保证线程安全的原因
例:主线程修改fail
的值,从线程去判断,最终 System.out.println("end");
不会执行,因为从线程启动时会把fail
生成一个副本,以后执行的时候只读副本;主线程修改了值从线程并不会知道。
public class Main extends Thread{
volatile boolean fail = false;
@Override
public void run() {
System.out.println("start");
while (!fail) {
}
System.out.println("end");
}
public static void main(String[] args) throws InterruptedException {
Main main = new Main();
main.start();
Thread.sleep(2000);
main.fail = true;
System.out.println("main thread end");
}
}
注:如果在while
循环中执行了System.out.println
该线程会刷新一次副本,导致该线程会读取到最新的值,因为println
方法中执行了synchronized
代码块,synchronized
有可见性功能。
将fail
增加volatile
关键字后可直接查看效果。