详解Java多线程与高并发(一)__synchronized关键字

本文详细介绍了Java中的synchronized关键字,包括其作用、使用方法、锁对象、线程同步原理、可重入特性、异常处理以及锁对象变更等问题。synchronized用于解决多线程的线程安全问题,通过锁对象确保数据的安全性。建议在大型高并发项目中使用同步代码块,避免不必要的锁竞争。此外,还讨论了静态同步方法、锁的重入性以及锁与异常的关系,帮助读者深入理解Java并发编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

synchronized在JDK1.5版本开始,尝试优化。到JDK1.7版本后,优化效率已经非常好了。在绝对效率上,不比reentrantLock差多少。

为什么要使用synchronize关键字?   

答:这涉及到了多线程的线程同步问题

当多个线程访问同一个数据时,容易出现线程安全问题。需要让线程同步,保证数据安全,即当两个或两个以上线程访问同一资源时,需要某种方式来确保资源在某一时刻只被一个线程使用只有多个 synchronized 代码块或同步方法使用的是同一个监视器对象,这些 synchronized 代码块之间才具有线程互斥的效果,这样可以保证代码块或同步方法内的代码的原子性,即从代码的开始到结束,不可分割。

synchronized锁什么?   

答:锁对象。

可能锁对象包括: this, 临界资源对象(多个线程都可以访问到的对象),Class类对象。

其中,synchronized(this)代码块和synchronized方法都是锁当前对象。

 

synchronized的使用方法分为两种:

1、同步方法:

    public void testSync2(){
		synchronized (this) {    //锁的是当前的类对象
			System.out.println(Thread.currentThread().getName() 
					+ "count = " + count++);
		}
    }

2、同步代码块:

        synchronized (o) {    //锁的是object这个对象
			System.out.println(Thread.currentThread().getName() 
					+ "count = " + count++);
        }

注意:

大型的高并发的项目中,建议使用同步代码块,非常不建议使用同步方法,这是因为同步方法不管方法内是否需要同步,直接加锁,而同步代码块可以使同步局部化,要求同步的时候才去加锁。锁尽量是同步代码块外部传入的,多线程共享的临界资源对象。

 

多线程加锁的问题:

       如上图所示。用堆空间中对象作为锁来实现多线程之间的互斥,从而达到线程多线程线程同步的要求。一个线程对应一个线程栈帧,代码在栈帧中运行,栈帧中存着对堆空间中对象的引用,如:int类型的count和Object类型的o,对count这种基本数据类型,引用和数据同步保存在栈帧中。而对象类型会在jvm的堆空间中再开辟一块空间存对象。

       当synchronize的时候,如果使用了对象o作为锁对象,则当前线程会对堆空间中的对象上加锁,如栈帧01的线程对对象加锁,而多线程中默认一个对象只能被一个线程加锁,因此在栈帧01的线程释放锁之前,栈帧02的线程都无法对对象加锁,即无法访问synchronize同步方法或同步代码块,进入了阻塞状态。 

       注意:锁的是一个对象,只有当其他线程再去访问同步方法或同步代码块,对该对象加锁的时候,才会阻塞住其他线程

 

静态同步方法

 * 静态同步方法,同步方法前加static关键字,锁的是当前类型的类对象。在本代码中就是Test_02.class

    public static synchronized void testSync4(){
		System.out.println(Thread.currentThread().getName()
				+ "staticCount = " + staticCount++);
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (Exception e) {
			e.printStackTrace();
		}
    }

效果等同于,同步代码块锁当前类型的类对象

    public static void main(String[] args) {
		synchronized (Test02.class) {
			System.out.println(Thread.currentThread().getName()
					+ "staticCount = " + staticCount++);
		}
    }

 

锁的可重入问题:

* 锁可重入。 同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。

如下代码同步方法m1()和m2()锁定的都是this,即当前对象。当一个线程调用m1()方法时,又在m1()方法中调用了m2()方法,这是允许的,不会被阻塞。

public class Test_06 {
	synchronized void m1(){ // 锁this
		System.out.println("m1 start");
		try {
			TimeUnit.SECONDS.sleep(2);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		m2();
		System.out.println("m1 end");
	}
	
	synchronized void m2(){ // 锁this
		System.out.println("m2 start");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("m2 end");
}

同步方法之继承:

* 子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法。

 * 相当于锁的重入,还是同一个线程。

如下代码所示,Sub_Test_07 类继承了 Test_07类,并重写m()方法,并在其中调用父类的m()方法,这相当于锁的重入。

public class Test_07 {
	synchronized void m(){
		System.out.println("Super Class m start");
		try {
			TimeUnit.SECONDS.sleep(1);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("Super Class m end");
	}
	public static void main(String[] args) {
		new Sub_Test_07().m();
	}
}
 
class Sub_Test_07 extends Test_07{
	synchronized void m(){
		System.out.println("Sub Class m start");
		super.m();
		System.out.println("Sub Class m end");
	}
}

同步方法之 锁与异常:

 * 当同步方法中发生异常的时候,自动释放锁资源,即其他线程访问同步方法或同步代码块时,不会影响其他线程的执行可以对对象加锁,而不会进入阻塞状态。

 * 注意,同步业务逻辑中,如果发生异常如何处理。

 

锁对象变更问题:

 * 在锁未释放之前,修改锁对象引用,不会影响同步代码的执行,因为锁的是对象,不是引用

如下代码所示:Thread1执行m()方法,对同步代码块中的对象加锁,进入了同步代码块并不断执行,然后主线程中new了一个新对象,并用引用o指向新对象,然后Thread2也执行m()方法,那么Thread1和Thread2会互斥而导致Thread2会被阻塞吗,答案是不会,因为Thread1线程锁的真实的对象,而非引用,同步代码一旦加锁后,那么会有一个临时的锁引用(在栈帧中)指向锁对象,和真实的引用(o)无直接关联。因此Thread1和Thread2都可正常执行m()中的同步代码块,因为他们锁的是不同的对象,不会因此产生互斥。

public class Test_13 {
        Object o = new Object();
 
        void m(){
                System.out.println(Thread.currentThread().getName() + " start");
                synchronized (o) {
                        while(true){
                                try {
                                        TimeUnit.SECONDS.sleep(1);
                                } catch (InterruptedException e) {
                                        e.printStackTrace();
                                }
                        System.out.println(Thread.currentThread().getName() + " - " + o);
                }
                }
        }
        public static void main(String[] args) {
                final Test_13 t = new Test_13();
                new Thread(new Runnable() {
                        @Override
                        public void run() {
                                t.m();
                        }
                }, "thread1").start();
                try {
                        TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                        e.printStackTrace();
                }
                Thread thread2 = new Thread(new Runnable() {
                        @Override
                        public void run() {
                                t.m();
                        }
                }, "thread2");
                /** 
                改变了t对象中的引用o所对应的新对象,改变的是t.o。
                但是,thread1线程栈帧中的临时引用,仍然指向原来的对象。
                所以thread1和thread2不互斥,他们锁的是不同对象,thread1也可以正常继续运行
                */
                t.o = new Object();    
                thread2.start();
        }
}

 

同步方法之 常量问题:

 * 在定义同步代码块时,不要使用常量对象作为锁对象(你看到的是是多个引用,其实指向的都是同一个对象,常量池问题)。

String s1 = "hello";

String s2 = new String("hello");        // new关键字,一定是在堆中创建一个新的对象

 

以上就是关于synchronize关键字和锁,我的一些认识和理解,希望对你有所帮助,欢迎留言、交流、点赞、关注。谢谢!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值