提到synchronized关键字就会和同步、线程安全挂钩。什么线程?说到线程先提一下进程,进程是由系统进行资源分配合调度的一个独立单位。线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
先说下什么是同步和和线程安全。
举个栗子:
同步:公司有4个厕所,恰好坏了3个就只有一个是好的,老王速度快,抢到了这个坑,其他的同事去的晚就只能在外面排队。
异步:在老王蹲坑的时候,扫地阿姨正在里面打扫卫生,各干各的。这就是异步。上厕所有人看着,这就把”隐私”暴露在外面了, 你说尴尬不?所以也就也导致了线程的安全问题。
所以线程的安全问题也就是因为异步操作引起的,所以就得解决这个问题,把异步操作变成同步操作。也就是同步锁。
1. synchronized 同步锁
synchronized关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
(也就是上面说的没坏的那个厕所同时只能有一个人可以用 /滑稽)
先做个小测试
public class Test {
public static void main(String[] str) {
Test test = new Test();
Thread t1 = new Thread() {
public void run() {
test.printNum();
}
};
Thread t2 = new Thread() {
public void run() {
test.printNum();
}
};
t1.start();
t2.start();
}
public void printNum() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
//打印结果
1 2 1 2 3 4 5 6 3 4 5 6 7 7 8 8 9 10 9 10
开启两个线程同时执行打印,可以看出打印出两组从1到10的是混乱的。两个线程都玩各的,产生了不安全性。那怎么才能使其打印的正常呢。下面把代码改动一下:
public class Test {
//省略..
//第一种方式
public void printNum() {
synchronized (this) {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
//第二种方式 (打印结果和第一种方式一致)
public synchronized void printNum() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
//打印结果
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
上面可以看到在printNum的代码块中添加了synchronized (this),保证了线程的安全,当t1线程执行完后t2线程才会执行,打印顺序恢复正常。第一种方式被称为同步代码块
, 该方式是在方法内部使用大括号包裹使得一个代码块得到同步,当这个代码块正在被一个线程执行下,下一个线程等待。第二种方式被称为同步方法
, 是在方法中添加synchronized关键字。还有一种写法和第一种方式类似synchronized (非this对象)。
说完对象锁再继续往下看两个示例:
//省略..
// 第一种方式:给方法添加static和synchronized关键字
public static synchronized void printNum() {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
// 第二种方式:把this改为Test.class
public void printNum() {
synchronized (Test.class) {
for (int i = 1; i <= 10; i++) {
System.out.print(i + " ");
}
}
}
//打印结果
1 2 3 4 5 6 7 8 9 10 1 2 3 4 5 6 7 8 9 10
这个示例中,两种方式和第一种打印的结果一样,为什么要拆开测试呢。 这就说到了同步锁。
上面两个示例包括最后说的非this对象在同步锁中属于对象锁
。
下面两个示例我们叫做类锁
。
2. 对象锁 和 类锁
引用一篇博文里的相关资料. 原创连接没找到。
1.一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,
2.在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁);
3.如果这个时候同步对象的锁被其他线程拿走了,他(这个线程)就只能等了(线程阻塞在锁池等待队列中)。
4.取到锁后,他就开始执行同步代码(被synchronized修饰的代码);
5.线程执行完同步代码后马上就把锁还给同步对象,其他在锁池中等待的某个线程就可以拿到锁执行同步代码了。
6.这样就保证了同步代码在同一时刻只有一个线程在执行。
先说下锁的概念,以便能更方面的理解。
在Java程序运行时环境中,JVM需要对两类线程共享的数据进行协调,而这两类数据是被所有线程共享的:
① 保存在堆中的实例变量
② 保存在方法区中的类变量
Start---
方法区 (Method Area)
方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据
。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
堆(Heap)
堆是JVM中一块可自由分配给对象的区域。当我们谈论垃圾回收(garbage collection)时,我们主要回收堆(heap)的空间
。Java的普通对象存活在堆中。与栈不同,堆的空间不会随着方法调用结束而清空。因此,在某个方法中创建的对象,可以在方法调用结束之后,继续存在于堆中。这带来的一个问题是,如果我们不断的创建新的对象,内存空间将最终消耗殆尽。
栈(Stacks)
在Java中,JVM中的栈记录了线程的方法调用。每个线程拥有一个栈
。在某个线程的运行过程中,如果有新的方法调用,那么该线程对应的栈就会增加一个存储单元,即帧(frame)。在frame中,保存有该方法调用的参数、局部变量和返回地址。
Java的参数和局部变量只能是基本类型的变量(比如int),或者对象的引用(reference)。因此,在栈中,只保存有基本类型的变量和对象引用
。引用所指向的对象保存在堆中。(引用可能为Null值,即不指向任何对象)。当被调用方法运行结束时,该方法对应的帧将被删除,参数和局部变量所占据的空间也随之释放。线程回到原方法,继续执行。当所有的栈都清空时,程序也随之运行结束。
End---
在Java虚拟机中,每个对象和类在逻辑上都是和一个监视器
相关联的。 也可以说任意一个对象都拥有自己的监视器。对于对象来说,相关联的监视器保护 对象的实例变量
。对于类来说,监视器保护 类的类变量
。
当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法。为了实现监视器的排他性监视能力,Java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。线程访问实例变量或者类变量不需锁。如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。
类锁实际上用对象锁来实现
。当虚拟机装载一个class文件的时候,它就会创建一个java.lang.Class类的实例。当锁住一个对象的时候,实际上锁住的是那个类的Class对象。Synchronized先天具有重入性
, 即在同一锁程中,线程不需要再次获取同一把锁(一个线程可以多次对同一个对象上锁)。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
小小的总结一下:Synchronized代码执行前会进行抢锁的动作,抢到锁的线程执行代码块,没有抢到锁的线程等待(阻塞)其线程执行完后, 再进行抢锁操作。
- 并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 比较关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞,阻塞的意思就是不仅这个方法不能执行,这之后的方法也不能执行。
- 这三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码
- 以上规则对其它对象锁同样适用
最后看一下对象锁和类锁的区别: