一、概念
多个线程访问同一个资源时,需要对该资源上锁。即同时只允许一个线程访问该资源。任何线程要执行synchronized里的代码,都必须先拿到锁。synchronized底层实现,JVM并没有规定必须应该如何实现,Hotspot在对象头上(64位)拿出2位来记录该对象是不是被锁定,markword,即锁定的是某个对象。
每一个class文件加载到内存后,都会生成Class类的一个对象和加载到内存的代码对应,所以锁静态方法时,锁的是Class类的某个对象(比如T.class)。synchronized锁的都是对象。
1、同步方法和非同步方法可以同时执行
/**
* @author Java和算法学习:周一
*/
public class T {
public synchronized void m1() {
System.out.println(Thread.currentThread().getName() + " m1 start...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m1 end...");
}
public void m2() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " m2 end...");
}
public static void main(String[] args) {
T t = new T();
new Thread(()->t.m1(), "t1").start();
new Thread(()->t.m2(), "t2").start();
// new Thread(t::m1, "t1").start();
// new Thread(t::m2, "t2").start();
}
}
/**
* @author Java和算法学习:周一
*/
public class Account {
String name;
double money;
public synchronized void set(String name, double money) {
this.name = name;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.money = money;
}
public /*synchronized*/ double getMoney(String name) {
return money;
}
public static void main(String[] args) {
Account account = new Account();
new Thread(() -> account.set("zhang", 100.0)).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(account.getMoney("zhang"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(account.getMoney("zhang"));
}
}
2、synchronized是可重入锁
一个同步方法可以调用另外一个同步方法,一个线程拥有某个对象的锁,再次申请的时候任然会得到该对象的锁。
比如有个父类的方法加了synchronized,子类重写了该方法,并且在方法里调用了父类的方法,此时是能够调用得到的,如果不是可重入锁,显然是有问题的。
/**
* @author Java和算法学习:周一
*/
public class T {
public synchronized void m() {
System.out.println("m start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("m end...");
}
}
class Child extends T {
@Override
public synchronized void m() {
System.out.println("child m start...");
super.m();
System.out.println("child m end...");
}
public static void main(String[] args) {
new Child().m();
}
}
3、加锁的方法产生异常会释放锁
/**
* @author Java和算法学习:周一
*/
public class T {
int count;
public synchronized void m() {
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count=" + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 3) {
//此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch处理,然后让循环继续
int i = 1/0;
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(()->t.m(), "t1").start();
new Thread(()->t.m(), "t2").start();
}
}
4、synchronized(Object)
不能锁String常量、Integer、Long等
二、底层实现
JDK(1.5之前)早期的时候是重量级锁,都是找OS申请锁。
改进:锁升级
sync(this)
Hotspot的实现
1、第一个线程访问时,先在这个对象头上,markword记录这个线程的线程号,此时并没有加锁,这叫偏向锁(即偏向于你)。同一个线程再次访问时,直接使用就可以,效率很高。
2、第二个线程访问时(即有线程争用时),升级为自旋锁,占用CPU在等待着(不会进入CPU的等待队列),但不访问OS,所以是在用户态去解决锁的问题,不经过内核态,加锁和解锁的效率比经过内核态的高。
3、默认旋10次后锁再次升级,升级为重量级锁,即去OS申请资源,来加锁,此线程变为等待状态,进入CPU的等待队列里(不再占用CPU资源)。
4、锁只能升级,不能降级。
由此可见:
执行时间长 用重量级锁,
执行时间短 用自旋锁;
线程数多 用重量级锁,
线程数少 用自旋锁。
加锁代码执行时间长,线程数多 | 加锁代码执行时间短,线程数少 |
---|---|
用重量级锁 | 用自旋锁 |
三、锁的其他相关知识
1、锁的细化粗化
(1)锁的细化
例如count++前后都有一些业务逻辑,此时synchronized不用加在方法上,可以直接加在count++上。
(2)锁的粗化
比如一个方法里面有很多细的锁,则可以把锁粗化,即把锁加在方法上。
2、锁定对象的改变
锁定某个对象o,如果o的属性发生改变,不影响锁的使用;但是o变成另外一个对象,则锁定的对象发生改变。不想让o改变,则可以定义为final类型的。