synchronized
对于Java中的同步方法,一直以来是一个比较难以理解的问题
同步锁的作用主要有以下三个:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
(3)有效解决重排序问题。
从语法上讲,Synchronized总共有三种用法:
(1)修饰普通方法 锁是当前实例对象
(2)修饰静态方法 锁是当前类的class对象
(3)修饰代码块 锁是括号里面的对象
该锁具有可重入体现性,同一个对象里面可以多次使用synchronized。
但是在互斥的过程中还有以下小的细节需要注意,以下是常用的8中情况,当然这里只列出了synchronized修饰普通方法和静态类,修饰代码块没有列出。
线程操作资源类常见的8种情况:
*1 标准访问,请问先打印邮件还是短信
*2 暂停4秒钟在邮件方法,请问先打印邮件还是短信
*3 新增普通sayHello方法,请问先打印邮件还是hello
*4 两部手机,请问先打印邮件还是短信
*5 两个静态同步方法,同一部手机,请问先打印邮件还是短信
*6 两个静态同步方法,2部手机,请问先打印邮件还是短信
*7 1个静态同步方法,1个普通同步方法,同一部手机,请问先打印邮件还是短信
*8 1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
资源类
class Phone
{
public synchronized void sendEmail()throws Exception
{
System.out.println("*****sendEmail");
}
public synchronized void sendSMS()throws Exception
{
System.out.println("*****sendSMS");
}
public void sayHello()throws Exception
{
System.out.println("*****sayHello");
}
}
情况1:
Phone phone = new Phone();
//线程1
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
//Thread.sleep(100);
//线程2
new Thread(() -> {
try
{
phone.sendSMS();
//phone.sayHello();
//phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
最终输出结果:
*****sendEmail
*****sendSMS
或者:
*****sendSMS
*****sendEmail
对于这个结论也许对于初学者你有点不信,可以自行实验。
我们要理解的是:当线程启动之后,并且执行了start方法,并不是立马执行里面的代码,并不是说threadA 代码在前面,就一定是先调用A的方法,具体调用时机是由操作系统来自行判断的。如果都知道多线程就是线程之间来回切换工作,如果想要严格按照顺序执行,请看情况2
情况2
改造下发送邮件的方法,在里面暂停3秒中
public synchronized void sendEmail()throws Exception
{
Thread.sleep(3000);
System.out.println("*****sendEmail");
}
main方法中不变,查看执行结果:
执行了n多次,都是
*****sendEmail
*****sendSMS
类比生活中的例子:
手机的功能很多,我们就列举出来 发短信,发邮件这两种。
对于独占类型的操作,类似这两种,一部手机同一时间不可能既在发短信又在发邮件(杠精可能会说我做定时任务不就可以了,杠精圆润的走开),这里发短信,发邮件这个操作就可以类比成synchronized的方法。
情况1,2的得出的结论:
1.一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
2.synchronized锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法。
情况3
增加一个普通方法
public void say()throws Exception
{
System.out.println("*****hello world");
}
main方法中线程A中执行sendEmail并且sleep(3),线程B中执行say这个普通的方法。
执行结果:
*****hello world
*****sendEmail
继续类比手机的案例:
普通方法就类似于后台进程在检测网络,他执行的时候不需要单独占有整个手机,类似于后台检测网络,流量这些小行为。
结论3
普通方法后发现和同步锁无关,不影响同步锁内的方法执行。
情况4
class Phone
{
public synchronized void sendEmail()throws Exception
{
TimeUnit.SECONDS.sleep(3);
System.out.println("*****sendEmail");
}
public synchronized void sendSMS()throws Exception
{
System.out.println("*****sendSMS");
}
public void say()throws Exception
{
System.out.println("*****hello world");
}
}
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
//phone.sendSMS();
//phone.say();
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
打印结果:
这个其实也好理解:
对于1,2得出的结论,锁的是对象,两步手机相当于两个对象实例,他们之间是互不影响的。
因此即使线程A执行的sendEmail有休眠3秒,先执行的是线程B中的方法。
结论4
换成两个对象后,不是同一把锁了,对象之间不影响执行同步方法。
情况5
使用static修饰 synchronized
public static synchronized void sendEmail()throws Exception
{
TimeUnit.SECONDS.sleep(3);
System.out.println("*****sendEmail");
}
public static synchronized void sendSMS()throws Exception
{
System.out.println("*****sendSMS");
}
Phone phone = new Phone();
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
使用static修饰了synchronized之后,锁就不是当前的this对象了,而是当前类。
说白了类就是一个模板,对象是这个模板具体的实现。因此对于模板被锁住了,同一个对象执行的时候,还是和案例2中一样。
情况6
phone类保持和情况5一样,现在换两个对象。分别执行发邮件和发短信方法。
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
phone2.sendSMS();
//phone2.say();
//phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
既然是锁的是类模板,即使是两个类同一时间还是只能同一个访问。
情况7
Phone类保持5一样,main方法中:
Phone phone = new Phone();
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
phone.say();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
执行结果:
这里,就涉及到两把锁了,一个是当前实例对象this的锁,一个是当前类的锁,他们互不影响,因此还是按照顺序执行,哪个先得到锁哪个先运行。
情况8
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
//phone2.sendSMS();
phone2.say();
//phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
现在虽然是两个不同的锁,但是他们需要的锁还是不一样的,同情况7因此还是各自独立互不影响。
总结
-
- synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
-
- 具体表现为以下3种形式。
-
- 对于普通同步方法,锁是当前实例对象,锁的是当前对象this,
-
- 对于同步方法块,锁是Synchonized括号里配置的对象。
-
- 对于静态同步方法,锁是当前类的Class对象。