第二章:对象及变量的并发访问

时间:2020-12-01 16:08:17
为什么要使用多线程编程?什么时候会出现线程安全问题?
在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的的资源:一个变量、一个对象、一个文件、一个数据库表等,而当多个线程同时访问同一个资源的时候,就会存在一个问题:
  由于每个线程执行的过程是不可控的,所以很可能导致最终的结果与实际上的愿望相违背或者直接导致程序出错。
举个简单的例子:
  现在有两个线程分别从网络上读取数据,然后插入一张数据库表中,要求不能插入重复的数据。
  那么必然在插入数据的过程中存在两个操作:
  1)检查数据库中是否存在该条数据;
  2)如果存在,则不插入;如果不存在,则插入到数据库中。
  假如两个线程分别用thread-1和thread-2表示,某一时刻,thread-1和thread-2都读取到了数据X,那么可能会发生这种情况:
  thread-1去检查数据库中是否存在数据X,然后thread-2也接着去检查数据库中是否存在数据X。
  结果两个线程检查的结果都是数据库中不存在数据X,那么两个线程都分别将数据X插入数据库表当中。
  这个就是线程安全问题,即多个线程同时访问一个资源时,会导致程序运行结果并不是想看到的结果。

本章主讲synchronized同步关键字使用,让我们实现线程安全的程序,解决非线程安全问题。


本章目录:
1.1.内部变量不存在线程安全问题
1.2.全局变量导致非线程安全
1.3.使用synchronized进行同步
1.4.多个对象多个锁
1.5.脏读
1.6.将任意对象作为对象监视器
1.7.同步synchronized方法无限等待与解决
1.8.多线程死锁
1.9.死循环
2.0.使用关键字volatile更新数据
2.1.使用原子类进行修改操作
2.2.原子类也并不完全安全


1.1.内部变量不存在线程安全问题

当多个线程执行一个方法,方法内部的局部变量并不是临界资源,因为方法是在栈上执行的,而Java栈是线程私有的,因此不会产生线程安全问题。
class PrivateObject {

public void add(String username) {
int num = 0;
if ("a".equals(username)) {
num = 100;
System.out.println("a set over");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num = " + num);
}

}
class ThreadA extends Thread{
private PrivateObject object;

public ThreadA(PrivateObject object) {
super();
this.object = object;
}

@Override
public void run() {
super.run();
object.add("a");
}

}
class ThreadB extends Thread{
private PrivateObject object;

public ThreadB(PrivateObject object) {
super();
this.object = object;
}

@Override
public void run() {
super.run();
object.add("b");
}

}
public static void main(String[] args) {
PrivateObject privateObject = new PrivateObject();
ThreadA threadA = new ThreadA(privateObject);
threadA.start();
ThreadB threadB = new ThreadB(privateObject);
threadB.start();
}
运行结果:
a set over
b set over
b num = 200
a num = 100

很明显,num的值是正确的,如果ThreadA线程先抢了资源了那么执行的时候就是num = 100然后停了2秒后输出num的值,然后ThreadB线程执行,设置了num=200,那么输出来的值就是200了。
然而我们把int num = 0;放到全局变量的话就会变成1.2.
1.2.全局变量导致非线程安全
该例子把上面的对象修改一下:
class PrivateObject {
int num = 0;
public void add(String username) {
if ("a".equals(username)) {
num = 100;
System.out.println("a set over");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num = " + num);
}


}
运行结果:
a set over
b set over
b num = 200
a num = 200

假设ThreadA线程先被执行,那么全局变量num = 100,然后进行一个耗时操作,此时,ThreadB进入执行了更改了num的值编程200然后输出,此时num = 200,就在这个时候ThreadA耗时已经完成,然后输出他的值,发现输出来是 num = 200,因为这个时候num已经被ThreadB修改了,所以输出了 num = 200。那么如何解决非线程安全的问题呢?我们来看看1.3.
1.3.使用synchronized进行同步
public synchronized void add(String username) {
if ("a".equals(username)) {
num = 100;
System.out.println("a set over");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
num = 200;
System.out.println("b set over");
}
System.out.println(username + " num = " + num);
}
在该函数添加了synchronized之后你会发现输出的结果:
a set over
a num = 100
b set over
b num = 200

这个实例输出来的结果可以看出它是线程安全的。由于该实例是同步的,所以我们看到的结果如果是ThreadA先执行的话就会造成阻塞,后面进行等待。这种结果一样不是我们想要的,那么如何解决这种问题呢?我们来看1.4.


1.4.多个对象多个锁
public static void main(String[] args) {
PrivateObject privateObject1 = new PrivateObject();
PrivateObject privateObject2 = new PrivateObject();
ThreadA threadA = new ThreadA(privateObject1);
threadA.start();
ThreadB threadB = new ThreadB(privateObject2);
threadB.start();
}
创建多个实例让线程执行分成多路,这就是异步处理,我们可以看出不管ThreadA是否执行了耗时ThreadB还是会去执行,由于多个实例执行多个同步方法。结果输出:
a set over
b set over
b num = 200
a num = 100

1.5.脏读
有时候就算你加了同步还是会出现数据脏读现象。例如:
class Service {
private String name = "A";
private String password = "AA";
public synchronized void update(String name, String password){
try {
System.out.println("当前线程为:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "进入同步");
this.name = name;
Thread.sleep(3000);
this.password = password;
System.out.println("name = " + name + ", password = " + password);
System.out.println("当前线程为:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "离开同步");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public String select(){
return "name = " + name + ", password = " + password;
}

}


class MyThread extends Thread {

Service service;

public MyThread(Service service) {
super();
this.service = service;
}

@Override
public void run() {
super.run();
service.update("B", "BB");
}

}
public static void main(String[] args) {
Service service = new Service();
MyThread myThread = new MyThread(service);
myThread.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(service.select());
}
结果:
当前线程为:Thread-0 在 1467706088809进入同步
name = B, password = AA
name = B, password = BB
当前线程为:Thread-0 在 1467706091811离开同步

解决当前问题只能在读取的时候再加上对象锁,就不会有这样的情况了
public synchronized String select(){
return "name = " + name + ", password = " + password;
}
结果:
当前线程为:Thread-0 在 1467706347098进入同步
name = B, password = BB
当前线程为:Thread-0 在 1467706350105离开同步
name = B, password = BB
1.6.将任意对象作为对象监视器
class Service {
String string = new String();
public void update(){
synchronized(string){
try {
System.out.println("当前线程为:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "进入同步");
Thread.sleep(3000);
System.out.println("当前线程为:" + Thread.currentThread().getName() + " 在 " + System.currentTimeMillis() + "离开同步");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

}
public static void main(String[] args) {
Service service = new Service();
MyThread myThreadA = new MyThread(service);
myThreadA.start();
MyThread myThreadB = new MyThread(service);
myThreadB.start();
}
输出结果:
当前线程为:Thread-0 在 1467707013916进入同步
当前线程为:Thread-0 在 1467707016925离开同步
当前线程为:Thread-1 在 1467707016925进入同步
当前线程为:Thread-1 在 1467707019926离开同步

锁非this对象具有一定的优点:如果在一个类中有很多个synchronized方法,这时虽然能实现同步,但会收到阻塞,所以影响运行效率;但如果使用同步代码块锁非this对象,则synchronized(非this)代码块中的程序与同步方法是异步的,不与其它锁this同步方法争抢this锁,则可大大提高运行效率。


1.7.同步synchronized方法无限等待与解决
同步方法容易造成死循环。
class Service {
public synchronized void methodA(){
System.out.println("methodA begin");
boolean isContinueRun = true;
while(isContinueRun){

}
System.out.println("methodA end");
}

public synchronized void methodB(){
System.out.println("methodB begin");
System.out.println("methodB end");
}

}


class ThreadA extends Thread {
private Service service;

public ThreadA(Service service) {
this.service = service;
}

@Override
public void run() {
super.run();
service.methodA();
}


}


class ThreadB extends Thread {
private Service service;


public ThreadB(Service service) {
this.service = service;
}

@Override
public void run() {
super.run();
service.methodB();
}


}
public static void main(String[] args) {
Service service = new Service();
ThreadA threadA = new ThreadA(service);
threadA.start();
ThreadB threadB = new ThreadB(service);
threadB.start();
}
运行结果是死循环:
methodA begin
那么如何解决死循环呢?那就是不要使用同一对象监视器,也就是使用异步处理线程
class Service {
String a = "a";
public void methodA(){
synchronized (a) {
System.out.println("methodA begin");
boolean isContinueRun = true;
while(isContinueRun){

}
System.out.println("methodA end");
}
}


String b = "b";
public void methodB(){
synchronized (b) {
System.out.println("methodB begin");
System.out.println("methodB end");
}
}

}
这样就不会造成死锁了。
methodA begin
methodB begin
methodB end
1.8.多线程死锁
不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。下面我举例说明:
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("a".equals(username)){
synchronized (lock1) {
System.out.println("username = " + username);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a-----");
synchronized (lock2) {
System.out.println("按lock1 -> lock2代码顺序执行了");
}
}
}
if("b".equals(username)){
synchronized (lock2) {
System.out.println("username = " + username);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("b-----");
synchronized (lock1) {
System.out.println("按lock2 -> lock1代码顺序执行了");
}
}
}
}

}
public static void main(String[] args) {
DealThread t1 = new DealThread();
t1.setFlag("a");
Thread thread1 = new Thread(t1);
thread1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.setFlag("b");
Thread thread2 = new Thread(t1);
thread2.start();
}
运行结果:
username = a
username = b
a-----
b-----
线程1执行了a判断进入锁,锁了lock1对象进入耗时操作,另外一个线程2进入锁了lock2对象也进入了耗时操作,这时,lock1耗时已完毕执行了lock2操作,然而lock2对象还没有解锁,所以线程1被锁死无法释放而继续等待,这时线程2进入lock1对象锁,而lock1却已经发生死锁无法释放,这种情况之下两锁无法释放就会进入死锁状态。
如果服务器发生死锁了,那么我们也不用着急,因为jdk有提供检测是否有死锁现象的工具->jps。
C:\Users\Administrator.PC-20150302IVQW>jps
1984 Jps
3088 ThreadTest
5516

C:\Users\Administrator.PC-20150302IVQW>jstack -l 3088
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00d47074 (object 0x248f6c90, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00d46354 (object 0x248f6c98, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.xingqiba.DealThread.run(ThreadTest.java:111)
        - waiting to lock <0x248f6c90> (a java.lang.Object)
        - locked <0x248f6c98> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
"Thread-0":
        at com.xingqiba.DealThread.run(ThreadTest.java:97)
        - waiting to lock <0x248f6c98> (a java.lang.Object)
        - locked <0x248f6c90> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)

Found 1 deadlock.

使用jps命令我们可以查看死锁的id,然后使用jstack命令根据id查看该死锁的情况:
Found one Java-level deadlock:这句话表示发现1个java级别的死锁
"Thread-1":
  waiting to lock monitor 0x00d47074 (object 0x248f6c90, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00d46354 (object 0x248f6c98, a java.lang.Object),
  which is held by "Thread-1"
第一句话表示Thread-1这个名字的线程正在等待object,内存为0x248f6c90这个位置的地址,它是一个java.lang.Object对象,祈求Thread-0释放。
第一句话表示Thread-0这个名字的线程正在等待object,内存为0x248f6c98这个位置的地址,它是一个java.lang.Object对象,祈求Thread-1释放。
"Thread-1":
        at com.xingqiba.DealThread.run(ThreadTest.java:111)
        - waiting to lock <0x248f6c90> (a java.lang.Object)
        - locked <0x248f6c98> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
"Thread-0":
        at com.xingqiba.DealThread.run(ThreadTest.java:97)
        - waiting to lock <0x248f6c98> (a java.lang.Object)
        - locked <0x248f6c90> (a java.lang.Object)
        at java.lang.Thread.run(Unknown Source)
这两段话比较重要,它说明了两个死锁的位置,方便我们解决死锁的问题

学完synchronized同步块同步数据之后,我们来学一学另一个多线程同步关键字volatile
1.9.死循环
class PrintString {


private boolean isContinuePrint = true;


public boolean isContinuePrint() {
return isContinuePrint;
}


public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}


public void printStrngMethod() {
try {
while (isContinuePrint) {
System.out.println(isContinuePrint);
System.out.println("run printStringMethod threadName = "
+ Thread.currentThread().getName());
Thread.sleep(1000);
}
} catch (Exception e) {

}
}


}
public static void main(String[] args) {
PrintString printString = new PrintString();
printString.printStrngMethod();
System.out.println("我要停止它!stopThread="
+ Thread.currentThread().getName());
printString.setContinuePrint(false);
}
运行结果:
true
run printStringMethod threadName = main
true
run printStringMethod threadName = main
true
run printStringMethod threadName = main
true
run printStringMethod threadName = main

为什么会死循环呢?很明显,因为模块是在主线程调用,调用printStrngMethod()函数的时候进入了死循环,后面的代码块无法被执行,所以不断执行输出:
true
run printStringMethod threadName = main
那么如何解决问题呢?把代码放入Thread进行异步处理。

私有内存和公有内存是两个模块:
volatile private boolean isContinuePrint = true;
在私有内存中加关键字volatile关键字可以同步私有内存和公有内存。
public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}
如果在多线程中执行了死循环没有加volatile关键字就算你在主线程
public void setContinuePrint(boolean isContinuePrint) {
this.isContinuePrint = isContinuePrint;
}
设置了false,也是没有用的,因为你更新的是公有内存的栈,而私有的内存栈没有被修改,所以会出现死循环不停止。那么如何将公有内存的栈去更新私有的栈,去同步数据呢?在私有内存前加volatile关键字。

2.0.使用关键字volatile更新数据
class MyThread extends Thread{
volatile public static int count;

private static void addCount(){
for (int i = 0; i < 100; i++) {
count++;
}
System.out.println("count=" + count);
}

@Override
public void run() {
super.run();
addCount();
}

}
public static void main(String[] args) {
MyThread[] myThreads = new MyThread[100];
for (int i = 0; i < myThreads.length; i++) {
myThreads[i] = new MyThread();
}
for (int i = 0; i < myThreads.length; i++) {
myThreads[i].start();
}
}
运行结果:
count=8924
count=9024
count=9124
count=9224
count=9324
count=9424
count=9524
count=9624
count=9724
count=9824
count=9924
值出现了脏读了,可见volatile不能同步函数,那么如何解决这个问题呢?因为要同步函数,那么在函数前加synchronized。

2.1.使用原子类进行修改操作
class AddCountThread extends Thread {


private AtomicInteger count = new AtomicInteger(0);


@Override
public void run() {
super.run();
for (int i = 0; i < 100; i++) {
System.out.println(count.incrementAndGet());
}
}


}
public static void main(String[] args) {
AddCountThread countThread = new AddCountThread();
Thread t1 = new Thread(countThread);
t1.start();
Thread t2 = new Thread(countThread);
t2.start();
Thread t3 = new Thread(countThread);
t3.start();
Thread t4 = new Thread(countThread);
t4.start();
Thread t5 = new Thread(countThread);
t5.start();
}
运行结果:
492
493
494
495
496
497
498
499
500
这样值一样不会出现脏读现象了。

2.1.原子类也并不完全安全
class MyService {


public AtomicLong atomicLong = new AtomicLong();


public void addNum() {
System.out.println(Thread.currentThread().getName() + "加了100之后的值就是:"
+ atomicLong.addAndGet(100));
atomicLong.addAndGet(1);
}


}


class MyThread extends Thread {


MyService myService;

public MyThread(MyService myService) {
this.myService = myService;
}

@Override
public void run() {
super.run();
myService.addNum();
}

}
public static void main(String[] args) {
try {
MyService service = new MyService();
MyThread[] myThreads = new MyThread[10];
for (int i = 0; i < myThreads.length; i++) {
myThreads[i] = new MyThread(service);
}
for (int i = 0; i < myThreads.length; i++) {
myThreads[i].start();
}
Thread.sleep(1000);
System.out.println(service.atomicLong.get());
} catch (Exception e) {

}
}
运行结果:
Thread-0加了100之后的值就是:100
Thread-3加了100之后的值就是:300
Thread-1加了100之后的值就是:200
Thread-5加了100之后的值就是:403
Thread-7加了100之后的值就是:504
Thread-9加了100之后的值就是:605
Thread-2加了100之后的值就是:706
Thread-4加了100之后的值就是:807
Thread-6加了100之后的值就是:908
Thread-8加了100之后的值就是:1009
1010
值一样出现脏读现象,那么如何解决问题呢?一样使用synchronized关键字同步数据
public synchronized void addNum() {
System.out.println(Thread.currentThread().getName() + "加了100之后的值就是:"
+ atomicLong.addAndGet(100));
atomicLong.addAndGet(1);
}
运行结果:
Thread-1加了100之后的值就是:100
Thread-3加了100之后的值就是:201
Thread-5加了100之后的值就是:302
Thread-7加了100之后的值就是:403
Thread-9加了100之后的值就是:504
Thread-0加了100之后的值就是:605
Thread-2加了100之后的值就是:706
Thread-4加了100之后的值就是:807
Thread-6加了100之后的值就是:908
Thread-8加了100之后的值就是:1009
1010
关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或某一个代码块。它包含两个特征:互斥性和可见性。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护之前所有的修改效果。
学习多线程并发,要着重"外练互斥,内修可见",这是掌握多线程、学习多线程并发的重要技术点。