Java 并发内置锁 Synchronized.docx
- 文档编号:25957776
- 上传时间:2023-06-16
- 格式:DOCX
- 页数:20
- 大小:77.98KB
Java 并发内置锁 Synchronized.docx
《Java 并发内置锁 Synchronized.docx》由会员分享,可在线阅读,更多相关《Java 并发内置锁 Synchronized.docx(20页珍藏版)》请在冰豆网上搜索。
Java并发内置锁Synchronized
Java并发:
内置锁Synchronized
一.线程安全问题
在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个共享、可变资源的情况,这种资源可以是:
一个变量、一个对象、一个文件等。
特别注意两点,
共享:
意味着该资源可以由多个线程同时访问;
可变:
意味着该资源可以在其生命周期内被修改。
所以,当多个线程同时访问这种资源的时候,就会存在一个问题:
由于每个线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问。
举个数据脏读的例子:
//资源类
classPublicVar{
publicStringusername="A";
publicStringpassword="AA";
//同步实例方法
publicsynchronizedvoidsetValue(Stringusername,Stringpassword){
try{
this.username=username;
Thread.sleep(5000);
this.password=password;
System.out.println("method=setValue"+"\t"+"threadName="
+Thread.currentThread().getName()+"\t"+"username="
+username+",password="+password);
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
//非同步实例方法
publicvoidgetValue(){
System.out.println("method=getValue"+"\t"+"threadName="
+Thread.currentThread().getName()+"\t"+"username="+username
+",password="+password);
}
}
//线程类
classThreadAextendsThread{
privatePublicVarpublicVar;
publicThreadA(PublicVarpublicVar){
super();
this.publicVar=publicVar;
}
@Override
publicvoidrun(){
super.run();
publicVar.setValue("B","BB");
}
}
//测试类
publicclassTest{
publicstaticvoidmain(String[]args){
try{
//临界资源
PublicVarpublicVarRef=newPublicVar();
//创建并启动线程
ThreadAthread=newThreadA(publicVarRef);
thread.start();
Thread.sleep(200);//打印结果受此值大小影响
//在主线程中调用
publicVarRef.getValue();
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}/*Output(数据交叉):
method=getValuethreadName=mainusername=B,password=AA
method=setValuethreadName=Thread-0username=B,password=BB
*///:
~
由程序输出可知,虽然在写操作进行了同步,但在读操作上仍然有可能出现一些意想不到的情况,例如上面所示的脏读。
发生脏读的情况是在执行读操作时,相应的数据已被其他线程部分修改过,导致数据交叉的现象产生。
这其实就是一个线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。
这里面,这个资源被称为:
临界资源。
也就是说,当多个线程同时访问临界资源(一个对象,对象中的属性,一个文件,一个数据库等)时,就可能会产生线程安全问题。
不过,当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的私有栈中,因此不具有共享性,不会导致线程安全问题。
二.如何解决线程安全问题
实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。
即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问。
换句话说,就是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
在Java中,提供了两种方式来实现同步互斥访问:
synchronized和Lock。
本文主要讲述synchronized的使用方法,Lock的使用方法在下一篇博文中讲述。
三.synchronized同步方法或者同步块
在了解synchronized关键字的使用方法之前,我们先来看一个概念:
互斥锁,即能到达到互斥访问目的的锁。
举个简单的例子,如果对临界资源加上互斥锁,当一个线程在访问该临界资源时,其他线程便只能等待。
在Java中,可以使用synchronized关键字来标记一个方法或者代码块,当某个线程调用该对象的synchronized方法或者访问synchronized代码块时,这个线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,这个线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。
下面这段代码中两个线程分别调用insertData对象插入数据:
1)synchronized方法
publicclassTest{
publicstaticvoidmain(String[]args){
finalInsertDatainsertData=newInsertData();
//启动线程1
newThread(){
publicvoidrun(){
insertData.insert(Thread.currentThread());
};
}.start();
//启动线程2
newThread(){
publicvoidrun(){
insertData.insert(Thread.currentThread());
};
}.start();
}
}
classInsertData{
//共享、可变资源
privateArrayList
//对共享可变资源的访问
publicvoidinsert(Threadthread){
for(inti=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}/*Output:
Thread-0在插入数据0
Thread-1在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-1在插入数据1
Thread-1在插入数据2
*///:
~
根据运行结果就可以看出,这两个线程在同时执行insert()方法。
而如果在insert()方法前面加上关键字synchronized的话,运行结果为:
classInsertData{
privateArrayList
publicsynchronizedvoidinsert(Threadthread){
for(inti=0;i<5;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}/*Output:
Thread-0在插入数据0
Thread-0在插入数据1
Thread-0在插入数据2
Thread-1在插入数据0
Thread-1在插入数据1
Thread-1在插入数据2
*///:
~
从以上输出结果可以看出,Thread-1插入数据是等Thread-0插入完数据之后才进行的。
说明Thread-0和Thread-1是顺序执行insert()方法的。
这就是synchronized关键字对方法的作用。
不过需要注意以下三点:
1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。
这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。
2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。
这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,
3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。
2)synchronized同步块
synchronized代码块类似于以下这种形式:
synchronized(lock){
//访问共享可变资源
...
}
当在某个线程中执行这段代码块,该线程会获取对象lock的锁,从而使得其他线程无法同时访问该代码块。
其中,lock可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。
特别地,实例同步方法与synchronized(this)同步块是互斥的,因为它们锁的是同一个对象。
但与synchronized(非this)同步块是异步的,因为它们锁的是不同对象。
比如上面的insert()方法可以改成以下两种形式:
//this监视器
classInsertData{
privateArrayList
publicvoidinsert(Threadthread){
synchronized(this){
for(inti=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
//对象监视器
classInsertData{
privateArrayList
privateObjectobject=newObject();
publicvoidinsert(Threadthread){
synchronized(object){
for(inti=0;i<100;i++){
System.out.println(thread.getName()+"在插入数据"+i);
arrayList.add(i);
}
}
}
}
从上面代码可以看出,synchronized代码块比synchronized方法的粒度更细一些,使用起来也灵活得多。
因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。
而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。
3)class对象锁
特别地,每个类也会有一个锁,静态的synchronized方法就是以Class对象作为锁。
另外,它可以用来控制对static数据成员(static数据成员不专属于任何一个对象,是类成员)的并发访问。
并且,如果一个线程执行一个对象的非staticsynchronized方法,另外一个线程需要执行这个对象所属类的staticsynchronized方法,也不会发生互斥现象。
因为访问staticsynchronized方法占用的是类锁,而访问非staticsynchronized方法占用的是对象锁,所以不存在互斥现象。
例如,
publicclassTest{
publicstaticvoidmain(String[]args){
finalInsertDatainsertData=newInsertData();
newThread(){
@Override
publicvoidrun(){
insertData.insert();
}
}.start();
newThread(){
@Override
publicvoidrun(){
insertData.insert1();
}
}.start();
}
}
classInsertData{
//非staticsynchronized方法
publicsynchronizedvoidinsert(){
System.out.println("执行insert");
try{
Thread.sleep(5000);
}catch(InterruptedExceptione){
e.printStackTrace();
}
System.out.println("执行insert完毕");
}
//staticsynchronized方法
publicsynchronizedstaticvoidinsert1(){
System.out.println("执行insert1");
System.out.println("执行insert1完毕");
}
}/*Output:
执行insert
执行insert1
执行insert1完毕
执行insert完毕
*///:
~
根据执行结果,我们可以看到第一个线程里面执行的是insert方法,不会导致第二个线程执行insert1方法发生阻塞现象。
下面,我们看一下synchronized关键字到底做了什么事情,我们来反编译它的字节码看一下,下面这段代码反编译后的字节码为:
publicclassInsertData{
privateObjectobject=newObject();
publicvoidinsert(Threadthread){
synchronized(object){}
}
publicsynchronizedvoidinsert1(Threadthread){}
publicvoidinsert2(Threadthread){}
}
从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。
monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个进程对临界资源的访问。
对于synchronized方法,执行中的线程识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。
如果有异常发生,线程自动释放锁。
有一点要注意:
对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。
四.可重入性
一般地,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。
然而,由于Java的内置锁是可重入的,因此如果某个线程试图获得一个已经由它自己持有的锁时,那么这个请求就会成功。
可重入锁最大的作用是避免死锁。
例如:
publicclassTestimplementsRunnable{
//可重入锁测试
publicsynchronizedvoidget(){
System.out.println(Thread.currentThread().getName());
set();
}
publicsynchronizedvoidset(){
System.out.println(Thread.currentThread().getName());
}
@Override
publicvoidrun(){
get();
}
publicstaticvoidmain(String[]args){
Testtest=newTest();
newThread(test,"Thread-0").start();
newThread(test,"Thread-1").start();
newThread(test,"Thread-2").start();
}
}/*Output:
Thread-1
Thread-1
Thread-2
Thread-2
Thread-0
Thread-0
*///:
~
五.注意事项
1).内置锁与字符串常量
由于字符串常量池的原因,在大多数情况下,同步synchronized代码块都不使用String作为锁对象,而改用其他,比如newObject()实例化一个Object对象,因为它并不会被放入缓存中。
看下面的例子:
//资源类
classService{
publicvoidprint(StringstringParam){
try{
synchronized(stringParam){
while(rue){
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
//线程A
classThreadAextendsThread{
privateServiceservice;
publicThreadA(Serviceservice){
super();
this.service=service;
}
@Override
publicvoidrun(){
service.print("AA");
}
}
//线程B
classThreadBextendsThread{
privateServiceservice;
publicThreadB(Serviceservice){
super();
this.service=service;
}
@Override
publicvoidrun(){
service.print("AA");
}
}
//测试
publicclassRun{
publicstaticvoidmain(String[]args){
//临界资源
Serviceservice=newService();
//创建并启动线程A
ThreadAa=newThreadA(service);
a.setName("A");
a.start();
//创建并启动线程B
ThreadBb=newThreadB(service);
b.setName("B");
b.start();
}
}/*Output(死锁):
A
A
A
A
...
*///:
~
出现上述结果就是因为String类型的参数都是“AA”,两个线程持有相同的锁,所以线程B始终得不到执行,造成死锁。
进一步地,所谓死锁是指:
不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。
b).锁的是对象而非引用
在将任何数据类型作为同步锁时,需要注意的是,是否有多个线程将同时去竞争该锁对象:
1).若它们将同时竞争同一把锁,则这些线程之间就是同步的;
2).否则,这些线程之间就是异步的。
看下面的例子:
//资源类
classMyService{
privateStringlock="123";
publicvoidtestMethod(){
try{
synchronized(ock){
System.out.println(Thread.currentThread().getName()+"begin"
+System.currentTimeMillis());
lock="456";
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName()+"end"
+System.currentTimeMillis());
}
}catch(InterruptedExceptione){
e.printStackTrace();
}
}
}
//线程B
classThreadBextendsThread{
privateMyServiceservice;
publicThreadB(MyServ
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Java 并发内置锁 Synchronized 并发 内置