synchronized的简介:
在java中synchronized
关键字是同步锁
,同步锁是依赖于对象而存在的,而且每一个对象有且仅有一个同步锁。当我们调用某对象的synchronized
方法时,就获取了该对象的同步锁。例如,synchronized(obj)
就获取了obj 这个对象
的同步锁。
synchronized的原理:
Synchronized
是通过对象内部的一个叫做monitor
的锁来实现的,但是monitor
锁的本质又是依赖于底层的操作系统的互斥锁
(Mutex Lock
)来实现的,而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized
效率低的原因。这种依赖于操作系统互斥锁
(Mutex Lock
)所实现的锁我们称之为重量级锁
。
不使用并发手段会有什么后果?
package synchronize;
/**
* 消失的请求数
*/
public class DisappearRequest1 implements Runnable{
static int i = 0;
static DisappearRequest1 instance = new DisappearRequest1();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
// 同时启动线程t1与线程t2,然后调用run方法
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
i++;
}
}
}
通过运行以上代码,我们发现:两个线程同时a++
操作,最后结果会比预计的少。
原因:
count++
, 看上去只是一个操作, 实际上包含了三个操作,它并非原子性
操作:
- 读取
count
- 将
count
加 1 - 将
count
的值写入内存中
在多线程环境下,任何一步执行完都有可能被打断, 都有可能轮到第二个线程去执行。假设目前我们的count
的值是9
,当第一个线程执行完count++
操作时,正好被第二个线程打断,而此时第一个线程执行完count++
操作后并没有把值写入内存,所以此时第二个线程在读取count
的值时,count
的值依旧是9
。按正常逻辑来讲,两次count++
操作后,count
的值为11
,然而在这样的一种情况下,两次count++
操作后,count
的值只是10
。对于这样的一种现象,我们称作线程不安全
。
synchronized的两个用法:
对象锁
包括方法锁
(默认锁对象为this当前实例对象
)和同步代码块锁
(自己指定锁对象)
代码块形式
手动指定锁对象
package synchronize;
/**
* 描述 对象锁实例1,代码块形式
*/
public class SynchronizedObjectCodeBlock2 implements Runnable {
static SynchronizedObjectCodeBlock2 instance = new SynchronizedObjectCodeBlock2();
// 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
@Override
public void run() {
// this值为SynchronizedObjectCodeBlock2对象
synchronized (this) {
System.out.println("this的值为:"+ this);
System.out.println("我是对象锁的代码块形式。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
public static void main(String[] args) {
System.out.println("instance实例对象:" + instance);
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
方法锁形式
synchronized
修饰普通方法, 锁对象默认为this
package synchronize;
/**
* 描述:对象锁实例2,方法锁形式
*/
public class SynchronizedObjectMethods3 implements Runnable {
static SynchronizedObjectMethods3 instance = new SynchronizedObjectMethods3();
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
@Override
public void run() {
methods();
}
// 当两个线程一起调用该方法时,线程会进入阻塞状态,从而达到串行的效果,以保证线程安全
public synchronized void methods(){
System.out.println("我是对象锁的方法修饰符形式,我叫"+ Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"运行结束");
}
}
类锁
指synchronized
修饰静态
的方法或指定锁
为Class对象
。在java类中可能有很多
个对象,但只有1
个Class
对象。所谓的类锁,其本质不过就是Class 对象
锁而已。
类锁与对象锁的区别
类锁
只能在同一时刻被一个对象拥有
。如果有多
个对象进行竞争
的话,线程与线程之间将会阻塞
;但对象锁
则不同,不同的对象锁
之间是不会有任何干扰
的,他们会并行执行
,并不会造成阻塞
。因为锁的当前对象是this
,同一个类
中,当锁对象
不一样的多个线程同时
运行时,会同时执行
;如果是类锁
,且两个对象都属于同一个类
,则意味着这两个对象共享同一把锁
,这样一来则会进行阻塞
。
形式一:
在static
方法上加synchroized
关键字。
package synchronize;
/**
* 描述:类锁的第一种形式:静态方法锁
*/
public class SynchronizedClassStatic4 implements Runnable {
static SynchronizedClassStatic4 instance1 = new SynchronizedClassStatic4();
static SynchronizedClassStatic4 instance2 = new SynchronizedClassStatic4();
@Override
public void run() {
methods();
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
// TODO why? 为什么methods方法加上 static 关键字就会造成阻塞?没加static关键字时,是一种的并行的状态?
// 原因是该方法加上static关键字后就会属于类锁(锁定的对象是class对象),
// 而class对象又是唯一的,要争取资源必须等待,所以先后执行了;
// 然而一旦去掉static关键字,methods方法则变成对象方法,因此不同的对象都执行各的run方法,
// 即锁(this)对象不同,所以则是并行执行。
//
public static synchronized void methods(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是类锁的第一形式:static形式,我叫"+ Thread.currentThread().getName());
}
}
形式二:
synchroized
(*.class)代码块。
package synchronize;
/**
* 描述:类锁的第二种形式:`synchroized`(*.class)代码块。
*/
public class SynchronizedClassClass5 implements Runnable {
static SynchronizedClassClass5 instance1 = new SynchronizedClassClass5();
static SynchronizedClassClass5 instance2 = new SynchronizedClassClass5();
@Override
public void run() {
try {
methods();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance2);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
// TODO 当使用 SynchronizedClassClass5.class 时,线程以串行的方式执行;
// 当我们使用this关键字进行替换时,线程差不多是以并行的方式来执行 ? 为什么呢 ?
private void methods() throws InterruptedException {
synchronized (SynchronizedClassClass5.class){
System.out.println("我是类锁的第二种形式:synchronized(*.class)。" +
"我叫"+ Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
}
思考?
-
两个线程同时访问
一个对象
的同步方法。- 会进行阻塞。原因是他们的锁对象是同一个,两者之间会形成互斥,以串行的方式执行。
-
两个线程访问的是
两个对象
的同步方法。- 这个时候,
synchroized
是不起作用的,它俩是并行执行,原因是他们的锁对象并不是同一个,两者之间并不会形成互斥。
- 这个时候,
-
两个线程访问的是
synchroized
的静态方法。- 锁生效,线程会进行阻塞,两者之间并会形成互斥,因为一旦在方法名上加上
static
关键字,则会构成类锁,多个对象共用一个方法。因此会造成阻塞。具体如SynchronizedClassStatic4
案例所示。
- 锁生效,线程会进行阻塞,两者之间并会形成互斥,因为一旦在方法名上加上
-
同时访问
同步
方法与非同步方法
(通过代码演示)。
package synchronize;
/**
* 描述: 同时访问同步方法与非同步方法,执行结果:同时开始,同时结束,可见非同步方法不受到影响
*/
public class SynchronizedYesAndNo6 implements Runnable {
static SynchronizedYesAndNo6 instance = new SynchronizedYesAndNo6();
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
methods1();
}else {
methods2();
}
}
// 同步方法
public synchronized void methods1(){
System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
// 非同步方法
public void methods2(){
System.out.println("我是没加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
- 访问同一个对象的不同的普通同步方法(代码如下)。
package synchronize;
/**
* 描述: 同时访问一个类的不同的普通同步方法
*/
public class SynchronizedDifferenceMethods7 implements Runnable {
static SynchronizedDifferenceMethods7 instance1 = new SynchronizedDifferenceMethods7();
// TODO 从执行结果来看,虽然没有指定锁对象,但本质上是指定了this作为锁的对象
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
methods1();
}else {
methods2();
}
}
// 同步方法
public synchronized void methods1(){
System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
// 同步方法
public synchronized void methods2(){
System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance1);
Thread t2 = new Thread(instance1);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
- 同时访问静态synchronized和非静态synchronized的方法
package synchronize;
/**
* 描述:同时访问静态synchronized和非静态synchronized的方法
*/
public class SynchronizedStaticAndNormal8 implements Runnable {
static SynchronizedStaticAndNormal8 instance = new SynchronizedStaticAndNormal8();
// TODO 从执行结果来看,两个线程同时开始,同时结束。这是因为两个线程走的是不同的方法,即线程锁对象并非是同一个。
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
// 第一个线程走methods1()方法
methods1();
}else {
// 第二个线程走methods2()方法
methods2();
}
}
// 非静态同步方法(该方法的锁对象为:对象实例本身)
public synchronized void methods1(){
System.out.println("我是非静态加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
// 静态同步方法(该方法的锁对象为:类锁)
public static synchronized void methods2(){
System.out.println("我是静态加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
- 方法抛出异常后,会释放锁吗?
- 和
lock
不一样,synchronized
在抛出异常后,会释放锁,而lock
则需要在异常的`finaly中进行释放。
- 和
package synchronize;
/**
* 描述: 方法抛弃后,会释放锁。
* 展示不抛出异常前和抛出异常后的对比:一旦抛出异常,第二个线程会立刻进入同步方法,意味着锁已经释放
*/
public class SynchronizedException9 implements Runnable {
static SynchronizedException9 instance = new SynchronizedException9();
@Override
public void run() {
if(Thread.currentThread().getName().equals("Thread-0")){
methods1();
}else {
methods2();
}
}
// 非静态同步方法
public synchronized void methods1(){
System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public synchronized void methods2(){
System.out.println("我是加锁方法。我叫" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
public static void main(String[] args) {
// TODO 这两个线程使用的是同一个对象,这意味着是同一个对象锁
Thread t1 = new Thread(instance);
Thread t2 = new Thread(instance);
t1.start();
t2.start();
while (t1.isAlive() || t2.isAlive()) {
}
System.out.println("finished");
}
}
总结:
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(如案例1和案例5)。
- 每个实例都对应有自己的一把锁,不同实例之间互不影响,例外:锁对象是
.class
以及synchronized
修饰的是static
方法的时候,所有对象共同使用一把类锁
(如2,3,4,6案例所示)。 - 在
synchronized
代码块中,无论是正常使用还是在执行过程中抛出异常,锁都会得到释放(如案例7所示)。
在这里我们在补充一下,当线程调用synchronized
关键字修饰的方法时,而这个方法中再次调用一个没有被synchronized
关键字修饰的其他方法时,这个时候还是线程安全吗?
答案是否定的。一旦离开synchronized
关键字修饰的方法,进入到另一个方法时,这个方法就有可以被多个线程同时访问。
网友评论