赞
踩
package com.test; /** * @author hequan * Date:2019/11/26 * Time:9:43 */ public class AccountingSycnBan implements Runnable{ static int i =0; public synchronized void increase(){ i++; } @Override public void run() { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSycnBan b= new AccountingSycnBan(); AccountingSycnBan c= new AccountingSycnBan(); Thread t1 = new Thread(b); Thread t2 = new Thread(c); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 输出结果:并不能保证 * 2000000 * 1974600 * 1989600 */ }
我们开了2个线程,但是作用于两个实例对象,都调用的increase方法;
这里我们要清楚,变量i是非原子性的,我们多线程同时操作同一个共享变量时候,是不能保证原子性的,还有其实sync是锁的是this对象,也就是AccountingSycnBan的实例,但是在测试方法中,我们是new了两个实例,然后同时操作increase方法,其实这时候是有两把锁,并不能保证原子性;所以结果不能保证是正确的;
这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了。其实这里我们只要new一次实例就行了。
那么如果这里我们就是new了两个实例,有办法保证原子性吗?
答案当然是有的,我们上面讲到了,这里我们锁的是实例对象,java中线程同步锁可以是任何对象,但是我们是否可以锁类字节码对象。这是不是唯一的?
package com.test; /** * @author hequan * Date:2019/11/26 * Time:9:43 */ public class AccountingSycnBan implements Runnable{ static int i =0; public synchronized static void increase(){ i++; } @Override public void run() { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSycnBan b= new AccountingSycnBan(); AccountingSycnBan c= new AccountingSycnBan(); Thread t1 = new Thread(b); Thread t2 = new Thread(c); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 输出结果: * 2000000 */ }
接着我们来验证下一个锁对象实例,一个锁类class对象,看下是不是能共存;
package com.test; /** * @author hequan * Date:2019/11/26 * Time:9:43 */ public class AccountingSycnBan implements Runnable{ static int i =0; public synchronized static void increase(){ i++; }public synchronized void increase2(){ i++; } @Override public void run() { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); increase2(); } } public static void main(String[] args) throws InterruptedException { AccountingSycnBan b= new AccountingSycnBan(); AccountingSycnBan c= new AccountingSycnBan(); Thread t1 = new Thread(b); Thread t2 = new Thread(c); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 输出结果: * 39998 * 40000 * 39999 */ }
我们发现这是共存的,但是结果不唯一;
此时两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,我们结果也验证了这点;
package com.test; /** * @author hequan * Date:2019/11/26 * Time:9:43 */ public class AccountingSycnBan implements Runnable{ static int i =0; public synchronized static void increase(){ i++; } @Override public void run() { synchronized (this) { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } } } public static void main(String[] args) throws InterruptedException { AccountingSycnBan b= new AccountingSycnBan(); Thread t1 = new Thread(b); Thread t2 = new Thread(b); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
当然除了锁this对象,也可换成AccountingSycnBan 的实例对象,或类字节码对象;
// instance实例对象 static AccountingSycnBan instance= new AccountingSycnBan(); synchronized (instance) { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } } // 类对象 synchronized (AccountingSycnBan.class) { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } }
内置锁 不需要自己释放锁,jvm会自动释放。 锁比较重。
我们要明白一点,如果要做到原子性的操作,肯定是要对一个变量进行操作处理,来标识线程的操作记录;(monitor)
首先我们的明白对象在jvm中怎么存储的?
64位jvm虚拟机 8 byte 倍数存储。
包含三部分:
对象头
属性变量(实例数据)
填充数据(对齐填充)
可以在下面参考的网页中了解monitorenter和monitorexit的作用,我就不盗用他们的话了,大致意思是,每个对象都有一个monitor监视器,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值减1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的。
ReenternLock,概念都是类似的,只是锁是自身维护了一个volatile int类型的变量,通过对它加一减一表示占有锁啊重入之类的概念。
注意,如果synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增加这个一个标识符,获取它的monitor,所以本质上是一样的。
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
package com.test; /** * @author hequan * Date:2019/11/26 * Time:9:43 */ public class AccountingSycnBan implements Runnable{ static AccountingSycnBan instance= new AccountingSycnBan(); static int i =0; public synchronized static void increase(){ i++; } @Override public void run() { synchronized (instance) { for (int j = 0; j < 10000; j++) { System.out.println(Thread.currentThread().getName()); increase(); } } } public static void main(String[] args) throws InterruptedException { //AccountingSycnBan b= new AccountingSycnBan(); Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
在线程运行(run方法)中间打断它。如果当前线程是非阻塞的,那么需要手动打断。
中断线程(实例方法)
public void Thread.interrupt()
判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted()
判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted()
public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { System.out.println("sleep....."); boolean interrupted = this.isInterrupted(); System.out.println("interrupted..."+interrupted); } } } }; t.start(); TimeUnit.SECONDS.sleep(2); //中断处于阻塞状态的线程 t.interrupt(); boolean interrupted = t.isInterrupted(); System.out.println("interrupted2..."+interrupted); } /**输出结果 直接中断阻塞的线程 *sleep..... *interrupted...false */
除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { @Override public void run() { while (true) { System.out.println("未被中断操作"); } } }; t.start(); TimeUnit.SECONDS.sleep(2); //中断处于阻塞状态的线程 t.interrupt(); } /**输出结果 未被中断 *未被中断操作 *未被中断操作 .... */
虽然我们调用了interrupt方法,但线程t并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:
public static void main(String[] args) throws InterruptedException { Thread t = new Thread() { @Override public void run() { while (true) { if (this.isInterrupted()) { System.out.println("手动中断线程"); break; } } System.out.println("已跳出循环,线程中断!"); } }; t.start(); TimeUnit.SECONDS.sleep(2); //中断处于阻塞状态的线程 t.interrupt(); /** * 输出结果 *手动中断线程 * 已跳出循环,线程中断! */ }
种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,优化如下;
public void run(){
try {
//判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下
package com.test; import java.util.concurrent.TimeUnit; public class SynchronizedBlocked implements Runnable{ public synchronized void f() { System.out.println("Trying to call f()"); while(true) // Never releases lock Thread.yield(); } /** * 在构造器中创建新线程并启动获取对象锁 */ public SynchronizedBlocked() { //该线程已持有当前实例锁 new Thread() { @Override public void run() { f(); // Lock acquired by this thread } }.start(); } @Override public void run() { //中断判断 while (true) { if (Thread.interrupted()) { System.out.println("中断线程!!"); break; } else { System.out.println("进入while。。"); f(); } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlocked sync = new SynchronizedBlocked(); Thread t = new Thread(sync); //启动后调用f()方法,无法获取当前实例锁处于等待状态 t.start(); TimeUnit.SECONDS.sleep(1); //中断线程,无法生效 t.interrupt(); } }
所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。
synchronized (this) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。