前记:师夷长技以自强
1.问题背景
在上一篇文章中我们已经讨论了线程具有异步运行的特性,因此当多线程同时访问同一个实例变量时就会引发脏读的问题。而这显然不是我们愿意看到的,解决办法也很简单,就是给访问该变量的程序部分加锁。多线程并发在一些追求效率的系统中常存在变量不可见的问题,由于变量的不可见也会导致程序运行的结果不是我们想要的。一句话,同步性和可见性问题是多线程中的两大重点内容,他们分别对应于synchronized和volitle关键字的使用。本文主要围绕了在各种情况下如何使用这两个关键字而展开的。
2.synchronized同步方法
2.1方法内变量是线程安全的
ex1:
class HasSelfPrivateNum{
public void addI(String username){
try {
int num = 0;
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=100
可以看到b的写覆盖对a是完全没有影响的。
2.2实例变量非线程安全
把上面的num变量改为实例变量
ex2:
class HasSelfPrivateNum{
private int num = 0;
public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRef);
threadA.start();
ThreadB threadB = new ThreadB(numRef);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=200
当然,解决办法也就是在addI函数前加synchronized,这里不再演示。
2.3给非静态方法加对象锁
如果同步方法属于多个对象,则每个方法属于不同的锁,因此其运行也是异步的。synchronized关键字加到static静态方法上是给Class类上锁,而synchronized关键字加到非static静态方法上是给对象上锁。
ex3:
class HasSelfPrivateNum{
private int num = 0;
synchronized public void addI(String username){
try {
if(username.equals("a")){
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
}else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username+" num="+num);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class ThreadA extends Thread{
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("a");
}
}
class ThreadB extends Thread{
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
numRef.addI("b");
}
}
public class Test{
public static void main(String[] args) {
HasSelfPrivateNum numRefA = new HasSelfPrivateNum();
HasSelfPrivateNum numRefB = new HasSelfPrivateNum();
ThreadA threadA = new ThreadA(numRefA);
threadA.start();
ThreadB threadB = new ThreadB(numRefB);
threadB.start();
}
}
output:
a set over!
b set over!
b num=200
a num=100
可见,addI被两个线程调用运行时都是异步交叉运行的。
2.4synchronized锁可重入
也就是说,当一个线程得到一个对象锁后,再次请求此对象时是可以再次得到该对象的锁的。因此,一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。
ex4:
class Service{
synchronized public void service1(){
System.out.println("service1");
service2();
}
synchronized public void service2(){
System.out.println("service2");
service3();
}
synchronized public void service3(){
System.out.println("service3");
}
}
class MyThread extends Thread{
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
public class Test{
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
output:
service1
service2
service3
假如不允许锁重入,那么程序将会进入死锁。
除此之外,子类的同步函数也可以重入从父类继承的函数。如下:
ex5:
class Main {
public int i = 10;
synchronized public void operateIMainMethod() {
try {
i--;
System.out.println("main print i=" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Sub extends Main {
synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println("sub print i=" + i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class MyThread extends Thread {
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
2.4异常自动释放锁
线程即使获得了锁,但在运行的过程中如果遇到异常就会自动释放锁。如下:
ex6:
class Service{
synchronized public void testMethod(){
if(Thread.currentThread().getName().equals("a")){
System.out.println("ThreadName = "+Thread.currentThread().getName()+" run beginTime="+ System.currentTimeMillis());
int i = 1;
while (i==1){
if((""+Math.random()).substring(0,8).equals("0.123456")){
System.out.println("ThreadName="+Thread.currentThread().getName()+" run exceptionTime="+System.currentTimeMillis());
Integer.parseInt("a");
}
}
}else {
System.out.println("Thread B run Time="+System.currentTimeMillis());
}
}
}
class ThreadA extends Thread{
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
class ThreadB extends Thread{
private Service service;
public ThreadB(Service service) {
this.service = service;
}
@Override
public void run() {
service.testMethod();
}
}
public class Test {
public static void main(String[] args) {
try {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
Thread.sleep(500);
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
output:
ThreadName = a run beginTime=1591535415742
ThreadName=a run exceptionTime=1591535416623
Exception in thread “a” Thread B run Time=1591535416624
java.lang.NumberFormatException: For input string: “a”
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Integer.parseInt(Integer.java:652)
at java.base/java.lang.Integer.parseInt(Integer.java:770)
at Service.testMethod(Test.java:9)
at ThreadA.run(Test.java:27)
可以看到,在线程a最先获得了对象锁,但因为异常而释放锁,由线程b获得。
3.synchronized同步代码块
当同步方法执行耗时很长时,其他线程都要等待其运行完毕,因此总体的运行时间将会很长。这个时候就要同步代码块来解决了。同步代码块是作用于一个语句块而不是整个方法,同步代码块提供多线程间的互斥访问。
**synchronized(this)**以当前对象为对象监视器。
**synchronized(非this)**当一个类中有很多synchronized方法时,虽然能实现同步,但会受阻塞而影响运行效率。使用同步代码块非this对象不与其他锁this同步方法争抢this锁,可大大提高运行效率。
synchronized(非this)的三个结论:
1).多个线程同时执行synchronized(x){}同步代码块呈同步效果。
2)当其他线程执行x对象中的synchronized同步代码块呈同步效果。
3)当其他线程执行x对象方法里面的synchronized(this)代码块时也呈同步效果。
多线程死锁
当线程相互等待对方释放锁时就会产生死锁。
ex7:
class DealThread implements Runnable{
public String username;
public Object lock1 = new Object();
public Object lock2 = new Object();
public void setFlag(String username){
this.username = username;
}
@Override
public void run() {
if(username.equals("a")){
synchronized (lock1){
try{
System.out.println("username = "+username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock2){
System.out.println("按lock1->lock2代码顺序执行了");
}
}
}
if(username.equals("b")){
synchronized (lock2){
try {
System.out.println("username = "+username);
Thread.sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
synchronized (lock1){
System.out.println("按lock2->lock1代码顺序执行了");
}
}
}
}
}
public class Test {
public static void main(String[] args) {
try {
DealThread t1 = new DealThread();
t1.setFlag("a");
Thread thread1 = new Thread(t1);
thread1.start();
Thread.sleep(100);
t1.setFlag("b");
Thread thread2 = new Thread(t1);
thread2.start();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
output:
username = a
username = b
可以看到此时程序进入了死锁状态。
4.volatile
volatile关键字是为了强制编译器从主内存中访问数据,其与synchronized的区别主要如下:
1)从性能来看volatitle更好。
2)从修饰的对象来看,volatile只能修饰于变量,而synchronized可以修饰方法以及代码块。
3)多线程访问volatile不会发生阻塞,而synchronized会,也即volatile不支持原子性。
5.总结
本文对synchronized和volatile关键字都做了讲解,volatile演示部分较少,但因为其功能简单所以略去。学过操作系统我们都知道,线程之间不仅有竞争,还有同步,那么下一篇文章讲介绍线程间的通信。