Java 支持多个线程同时访问一个对象或者对象的成员变量,关键字 synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
Java 多线程中的同步机制会对资源进行加锁,保证同一时间只有一个线程可以操作对应资源,避免多线程同时访问相同资源发生冲突。Synchronized 是 Java 中的关键字,是一种同步锁,可以实现同步机制。
Synchronized 修饰的对象有一下几种:
- 修饰一个类。作用范围是 Synchronized 后面括号括起来的部分,作用的对象是这个类的所有对象。
- 修饰普通方法。被修饰的方法称为同步方法,作用范围是整个方法。一个对象中的加锁方法只允许一个线程访问,但这种情况下锁的是访问该方法的实例对象,如果多个线程不同对象访问该方法,则无法保证同步。
- 修饰静态方法。由于静态方法是类方法,所以锁的是包含该方法的类,也就是类对象;如果多个线程不同对象访问该静态方法,可以保证同步。
- 修饰代码块。普通代码块如 Synchronized(obj),obj 可以为类中的一个属性,也可以是当前的对象,同步效果和修饰普通方法一样; Synchronized(obj.class) 静态代码块的同步效果和修饰静态方法类似。
- Synchronized 方法控制的范围较大,它会同步对象中的所有 Synchronized 方法的代码
- Synchronized 代码块控制范围较小,只会同步代码块中的代码,而位于代码块之外的代码可以被多个线程访问,Synchronized代码块更加灵活精确。
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
// code
}
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
// code
}
// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
Object o = new Object();
synchronized (o) {
// code
}
}
对象锁和类锁:
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的 class 对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个 class 对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的 class 对象。类锁和对象锁之间也是互不干扰的。
示例:
package com.wuc.generic.thread;
public class SynTest {
private long count = 0;
private Object obj = new Object();//作为一个锁
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/*用在同步块上*/
public void incCount() {
synchronized (obj) {
count++;
}
}
/*用在方法上*/
public synchronized void incCount2() {
count++;
}
/*用在同步块上,但是锁的是当前类的对象实例*/
public void incCount3() {
synchronized (this) {
count++;
}
}
//线程
private static class Count extends Thread {
private SynTest simplOper;
public Count(SynTest simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
simplOper.incCount();//count = count+10000
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest simplOper = new SynTest();
//启动两个线程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000
}
}
修饰类
其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象,如下代码:
class ClassName {
public void method() {
synchronized (ClassName.class) {
//todo
}
}
}
修饰普通方法
Synchronized 修饰一个方法就是在方法签名加 Synchronized,如下代码:
public synchronized void method() {
}
在定义接口方法时不能使用 Synchronized 关键字
构造方法不能使用 Synchronized 关键字,但可以使用 Synchronized 代码块来进行同步
Synchronized 不能被继承。如果子类覆盖了父类的被 Synchronized 关键字修饰的方法,那么子类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。
修饰静态方法
静态方法属于类而不属于对象,所以 Synchronized 修饰的静态方法锁定的是这个类的所有对象。
public synchronized static void method() {
}
错误的加锁和原因分析
代码示例:
// 类说明:错误的加锁和原因分析
public class TestIntegerSyn {
public static void main(String[] args) throws InterruptedException {
Worker worker = new Worker(1);
//Thread.sleep(50);
for (int i = 0; i < 5; i++) {
new Thread(worker).start();
}
}
private static class Worker implements Runnable {
private Integer i;
private Object o = new Object();
public Worker(Integer i) {
this.i = i;
}
@Override
public void run() {
synchronized (i) {
Thread thread = Thread.currentThread();
// System.out.println(thread.getName() + "--@"
// + System.identityHashCode(i));
i++;
System.out.println(thread.getName() + "-------" + i + "-@"
+ System.identityHashCode(i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + "-------" + i + "--@"
+ System.identityHashCode(i));
}
}
}
}
运行结果:
可以看出结果并没有按预计的 2,3,4,5,6输出。
原因:虽然我们对 i 进行了加锁,但是
但是当我们反编译这个类的 class 文件后,可以看到 i++实际是
Integer 对象源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
本质上是返回了一个新的 Integer 对象。也就是每个线程实际加锁的是不同的 Integer 对象。synchronized (i) 锁的对象 i 是变化的,故结果错误。
解决:
synchronized (i) 改为 synchronized (o) 即可。锁的对象o是不变的。
synchronized 的不足之处
- 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,同一时间只能有一个线程执行。
- synchronized 无法知道线程有没有成功获取到锁
- 使用 synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
volatile 最轻量的同步机制
volatile 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某 个变量的值,这新值对其他线程来说是立即可见的。
参见代码:
/**
* 类说明:演示Volatile的提供的可见性
*/
public class VolatileCase {
private volatile static boolean ready;
private static int number;
private static class PrintThread extends Thread {
@Override
public void run() {
System.out.println("PrintThread is running.......");
while (!ready) ;//无限循环
System.out.println("number = " + number);
}
}
public static void main(String[] args) {
new PrintThread().start();
SleepTools.second(1);
number = 51;
ready = true;
SleepTools.second(5);
System.out.println("main is ended!");
}
}
不加 volatile 时,子线程无法感知主线程修改了 ready 的值,从而不会退出循环, 而加了 volatile 后,子线程可以感知主线程修改了 ready 的值,迅速退出循环。 但是 volatile 不能保证数据在多个线程下同时写时的线程安全。
不加 volatile 时效果图:
加 volatile 时效果图:
volatile 不能保证数据在多个线程下同时写时的线程安全,参见代码:
public class NotSafe {
private volatile long count = 0;
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
//count进行累加
public void incCount() {
count++;
}
//线程
private static class Count extends Thread {
private NotSafe simplOper;
public Count(NotSafe simplOper) {
this.simplOper = simplOper;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
simplOper.incCount();
}
}
}
public static void main(String[] args) throws InterruptedException {
NotSafe simplOper = new NotSafe();
//启动两个线程
Count count1 = new Count(simplOper);
Count count2 = new Count(simplOper);
count1.start();
count2.start();
Thread.sleep(50);
System.out.println(simplOper.count);//20000?
}
}
volatile 最适用的场景:一个线程写,多个线程读。