使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时。
synchronized(this)同步代码块
当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块才能执行该代码块。
一个线程访问object的一个同步代码块时,另外一个线程仍然可以访问该对象的非同步代码块。 以下代码可以证明:不在synchronized块中就是异步执行,在synchronized块中就是同步执行。
class Task{
public void doLongTimeTask(){
for (int i=0;i<5;i++){
System.out.println("nonsync threadName:"+Thread.currentThread().getName()+" i: "+i);
}
System.out.println();
synchronized (this){
for (int i=0;i<5;i++){
System.out.println("sync threadName:"+Thread.currentThread().getName()+" i: "+i);
}
}
}
}
class MyThread extends Thread{
private Task task;
public MyThread(Task task){
this.task=task;
}
@Override
public void run() {
task.doLongTimeTask();
}
}
public class Run {
public static void main(String[] args) {
Task task=new Task();
MyThread t1=new MyThread(task);
MyThread t2=new MyThread(task);
t1.start();
t2.start();
}
}
复制代码
运行结果为:
nonsync threadName:Thread-0 i: 0
nonsync threadName:Thread-1 i: 0
nonsync threadName:Thread-0 i: 1
nonsync threadName:Thread-1 i: 1
nonsync threadName:Thread-1 i: 2
nonsync threadName:Thread-1 i: 3
nonsync threadName:Thread-0 i: 2
nonsync threadName:Thread-1 i: 4
nonsync threadName:Thread-0 i: 3
nonsync threadName:Thread-0 i: 4
sync threadName:Thread-1 i: 0
sync threadName:Thread-1 i: 1
sync threadName:Thread-1 i: 2
sync threadName:Thread-1 i: 3
sync threadName:Thread-1 i: 4
sync threadName:Thread-0 i: 0
sync threadName:Thread-0 i: 1
sync threadName:Thread-0 i: 2
sync threadName:Thread-0 i: 3
sync threadName:Thread-0 i: 4
复制代码
可以看到,非同步代码块是异步执行的,同步代码块则是排队执行的。
同步代码块之间的同步性:在使用synchronized(this)代码块时,当一个线程访问object的一个同步代码块时,其他线程对同一个对象中所有其他synchronized(this)代码块的访问会被阻塞,说明synchronized使用的“对象监视器是一个”。
synchronized(this)代码块是锁定当前对象的。也就是说,多个线程调用同一个对象的不同名称的synchronized同步方法或者synchronized(this)同步代码块时,调用的效果就是按顺序执行,就是同步的,阻塞的。也就是说,无论是同步方法或者synchronized(this)代码块,都可以对其他同步方法或代码块调用呈阻塞状态。同一时间只有一个线程可以执行同步方法或同步代码块中的代码。
synchronized(非this对象)代码块
除了synchronized(this)同步代码块,Java还支持对任意对象作为“对象监视器”来实现同步的功能。任意对象大多数是实例变量及方法的参数。同样的,在多个线程持有“对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this)同步代码块中的代码。
锁非this对象有一定的优点:如果在一个类中有很多synchronized方法,这时虽然能实现同步,但会收到阻塞,影响运行效率。但是如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以提高运行效率。
在使用synchronized(非this对象)同步代码块时,对象监视器必须是同一个对象,如果不是同一个对象监视器,运行的结果就是异步调用了。示例代码:
package ch02.t12;
class Service {
private String anyStr = new String();
public void method() {
try {
synchronized (anyStr) {
System.out.println("Thread: " + Thread.currentThread().getName()
+ " time: " + System.currentTimeMillis() + "进入同步块");
Thread.sleep(3000);
System.out.println("Thread: " + Thread.currentThread().getName()
+ " time: " + System.currentTimeMillis() + "离开同步块");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread{
private Service service;
public MyThread(Service service){
this.service=service;
}
@Override
public void run() {
service.method();
}
}
public class Run {
public static void main(String[] args) {
Service service=new Service();
MyThread a=new MyThread(service);
a.setName("A");
a.start();
MyThread b=new MyThread(service);
b.setName("B");
b.start();
}
}
复制代码
运行结果为:
Thread: A time: 1544337130842进入同步块
Thread: A time: 1544337133842离开同步块
Thread: B time: 1544337133842进入同步块
Thread: B time: 1544337136852离开同步块
复制代码
如果将Service改成:
class Service {
public void method() {
try {
String anyStr = new String();
synchronized (anyStr) {
System.out.println("Thread: " + Thread.currentThread().getName()
+ " time: " + System.currentTimeMillis() + "进入同步块");
Thread.sleep(3000);
System.out.println("Thread: " + Thread.currentThread().getName()
+ " time: " + System.currentTimeMillis() + "离开同步块");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
运行结果为:
Thread: B time: 1544337317933进入同步块
Thread: A time: 1544337317933进入同步块
Thread: A time: 1544337320933离开同步块
Thread: B time: 1544337320933离开同步块
复制代码
是异步进行的,因此此时两个anyStr对象不是同一个对象了。
synchronized代码块的“脏读”
同步代码块放在非同步方法中进行生命,并不能摆正调用方法的线程的执行同步、顺序性。线程调用方法的顺序是无序的,虽然在同步块中执行的顺序是同步的,这样容易出现“脏读”问题。
class MyOneList{
private ArrayList list=new ArrayList();
synchronized public void add(String data){
list.add(data);
}
synchronized public int getSize(){
return list.size();
}
}
class MyService{
public MyOneList add(MyOneList list,String data){
try {
System.out.println(Thread.currentThread().getName());
if(list.getSize()<1){//保证list只有一个元素
Thread.sleep(2000);
list.add(data);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return list;
}
}
class MyThread extends Thread{
private MyOneList list;
public MyThread(MyOneList list){
this.list=list;
}
@Override
public void run() {
MyService service=new MyService();
service.add(list,"A");
}
}
public class Run {
public static void main(String[] args) throws InterruptedException {
MyOneList list=new MyOneList();
MyThread t1=new MyThread(list);
t1.setName("A");
t1.start();
MyThread t2=new MyThread(list);
t2.setName("B");
t2.start();
Thread.sleep(6000);
System.out.println(list.getSize());
}
}
复制代码
运行结果:
B
A
2
复制代码
首先可以发现,两个线程打印的顺序是无序的,说明线程的执行时异步的。返回的结果中list的size为2,原因是在add()方法中,list.getSize()这一方法是异步调用的。因此,需要对add()方法进行同步化,修改如下:
class MyService {
public MyOneList add(MyOneList list, String data) {
try {
synchronized (list) {
System.out.println(Thread.currentThread().getName());
if (list.getSize() < 1) {//保证list只有一个元素
Thread.sleep(2000);
list.add(data);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return list;
}
}
复制代码
list对象在项目中只有一份实例,且对其进行调用,因此以list参数进行同步处理。结果如下:
A
B
1
复制代码
synchronized(非this对象x)的三个结论
- 当多个线程同时执行synchronized(x)同步代码块时是同步的;
- 当线程A执行以x为锁的代码块时,其他线程执行x对象中的同步方法时也是同步的;
- 当线程A执行以x为锁的代码块时,其他线程执行x对象中的synchronized(this)代码块时也是同步的。
静态synchronized方法与synchronized(class)代码块
关键字synchronized还可以应用在static静态方法上,这样是堆当前的*.java文件对应的Class类加锁。
class Service{
synchronized public static void printA(){
try {
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printA方法");
Thread.sleep(3000);
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printA方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized public static void printB(){
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printB方法");
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printB方法");
}
synchronized public void printC(){
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 进入printC方法");
System.out.println("thread: "+Thread.currentThread().getName()+" time: "+System.currentTimeMillis()+" 离开printC方法");
}
}
class ThreadA extends Thread{
private Service service;
public ThreadA(Service service){
this.service=service;
}
@Override
public void run() {
service.printA();
}
}
class ThreadB extends Thread{
private Service service;
public ThreadB(Service service){
this.service=service;
}
@Override
public void run() {
service.printB();
}
}
class ThreadC extends Thread{
private Service service;
public ThreadC(Service service){
this.service=service;
}
@Override
public void run() {
service.printC();
}
}
public class Run {
public static void main(String[] args) {
Service service=new Service();
ThreadA a=new ThreadA(service);
a.setName("A");
a.start();
ThreadB b=new ThreadB(service);
b.setName("B");
b.start();
ThreadC c=new ThreadC(service);
c.setName("C");
c.start();
}
}
复制代码
运行结果:
thread: A time: 1544341920925 进入printA方法
thread: C time: 1544341920925 进入printC方法
thread: C time: 1544341920925 离开printC方法
thread: A time: 1544341923925 离开printA方法
thread: B time: 1544341923925 进入printB方法
thread: B time: 1544341923925 离开printB方法
复制代码
可以看出,A与C并不是同步的,A与B是同步的。原因就是A和B是持有的同一个Class锁,而C持有的是对象锁。
但是,Class锁可以对类的所有对象实例起作用。修改上述的代码,只修改main方法如下:
public class Run {
public static void main(String[] args) {
Service service1=new Service();
Service service2=new Service();
ThreadA a=new ThreadA(service1);
a.setName("A");
a.start();
ThreadB b=new ThreadB(service2);
b.setName("B");
b.start();
}
}
复制代码
运行结果如下:
thread: A time: 1544342122668 进入printA方法
thread: A time: 1544342125668 离开printA方法
thread: B time: 1544342125668 进入printB方法
thread: B time: 1544342125668 离开printB方法
复制代码
可以看到,线程A和线程B虽然调用的是不同的对象,但是由于二者是Class锁,因此仍然是同步进行的。
synchronized(class)代码块和synchronized static方法的作用是一样的,修改上述的printA()和printB()方法如下:
class Service {
public static void printA() {
synchronized (Service.class) {
try {
System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printA方法");
Thread.sleep(3000);
System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printA方法");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
synchronized public static void printB() {
synchronized (Service.class) {
System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 进入printB方法");
System.out.println("thread: " + Thread.currentThread().getName() + " time: " + System.currentTimeMillis() + " 离开printB方法");
}
}
}
复制代码
Run类和刚刚修改后的两个不同的Service对象一样,运行结果为:
thread: A time: 1544342263970 进入printA方法
thread: A time: 1544342266970 离开printA方法
thread: B time: 1544342266970 进入printB方法
thread: B time: 1544342266970 离开printB方法
复制代码
可以看到效果是一样的。
synchronized(string)同步代码块
在JVM中,具有String常量池缓存功能。在将synchronized(string)使用时,需要注意常量池带来的一些例外。在大多数情况下,同步代码块不推荐使用String作为锁对象,而改用其他,比如new Object(),它不放入缓存中。
多线程的死锁
死锁的场景一般是:线程 A 和线程 B 都在互相等待对方释放锁,或者是其中某个线程在释放锁的时候出现异常如死循环之类的。这时就会导致系统不可用。
常用的解决方案如下:
- 尽量一个线程只获取一个锁。
- 一个线程只占用一个资源。
- 尝试使用定时锁,至少能保证锁最终会被释放。
内置类与静态内置类
判断是否同步的方法是一样的,都是判断是否是同一个对象为锁。
锁对象的改变
如果锁对象本身改变了(如String从“a”变成了“b”),则改变前和改变后,线程的锁不一样,是异步的。但是如果锁的属性改变了,但是锁对象本身没变,则仍然是同步的。
参考资料
- 高洪岩. Java多线程编程核心技术[M]. 机械工业出版社, 2015.
- github.com/CyC2018/CS-…
- github.com/Snailclimb/…
- crossoverjie.top/JCSprout/#/…