一:回顾多线程在JVM中运行模型
这里直接将(二)中的图复制过来吧。
在回顾一下多线程运行快的原因吧。每一个线程都会复制一个工作副本以供本线程操作使用,在线程运行结束后再将工作副本同步到主内存中。
优点:每一个线程独立运行,效率贼快。
缺点:对于公共变量的处理,会产生"脏读"的可能。
二:多线程数据安全的几大特性
也许我们在接触这个问题之前应该会接触到数据库的事务的问题,数据库的事务具有以下的特性:一致性(分布式事务),隔离性(分布式锁),原子性(与一致性保持一致事务),持久性(数据库容灾处理)。同样多线程也拥有类似的几大特性(其实很好理解这2者的共同性:一般数据库是磁盘运存,二多线程则是内存的运存)。
1.共享性
对于一个多线程而言,数据共享是导致多线程的锁(数据安全)产生的最根本原因。可以这么说数据的共享是导致多线程多发问题的根本原因。下面我们看一下多线程下数据共享产生的问题:
package project;
public class test {
/**
* 共享数据
*/
public static Integer count = 0;
public static void main(String[] args) {
//启用10个线程对共享数据 count 都进行100次自增
for(int i =0 ;i < 10 ; i++ ) {
new Thread(new Runnable() {
@Override
public void run() {
try {
//进入的时候暂停1毫秒,增加并发问题出现的几率
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j =0 ; j< 100 ;j++) {
addOne();
}
}
}).start();
}
try {
//主程序暂停3秒,以保证上面的程序执行完成
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("按照理想状态,这里的count应该是1000,到那时我们来看一下实际结果:"+count);
}
public static void addOne() {
count++;
}
}
运行结果:有0,349,1000等等,明显不符合我们的期望。为什么会出现这种情况呢? 因为,A线程在修改的过程中,B线程也在修改最后导致结构紊乱。这就是数据共享在多线程中发生的问题。
2.互斥性
数据互斥是指在同时只允许对数据操作。如果一个数据为全局不可变的数据(比如常量),那么就无需考虑互斥了,因为任何操作不会导致改数据改变。其次如果操作对于某一个数据只有读操作,没有其他任何修改的操作,也可以不考虑互斥性(严格意义上说,这种情况也属于第一种情况),Java对于互斥性体现为解决方案(使用锁关键字)
package project;
public class test {
/**
* 共享数据
*/
public static Integer count = 0;
public static void main(String[] args) {
//启用10个线程对共享数据 count 都进行100次自增
for(int i =0 ;i < 10 ; i++ ) {
new Thread(new Runnable() {
@Override
public void run() {
try {
//进入的时候暂停1毫秒,增加并发问题出现的几率
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int j =0 ; j< 100 ;j++) {
addOne();
}
}
}).start();
}
try {
//主程序暂停3秒,以保证上面的程序执行完成
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("按照理想状态,这里的count应该是1000,到那时我们来看一下实际结果:"+count);
}
public synchronized static void addOne() {
count++;
}
}
3.原子性
原子性就是对数据的操作是一个不可中断的过程,在一个线程操作中,对数据的操作有多个步骤,其中每一个步骤都要会写到内存中,这样多个线程之间机会产生问题:
4.可见性
可见性实则是针对jvm虚拟机而言的,在文章的一开头,我们就谈到多线程在JVM中运行模型,不多说。
四:多线程数据问题的解决纲领
对于上述的多线程几大问题产生的解决方案一般分为2中:乐观锁和悲观锁
1.悲观锁
默认所有的操作都会互相影响,屏蔽一切违反数据完整性的操作。这时候就要用到java中的锁的关键字。
2.乐观锁
默认所有的线程操作多不会影响,只在数据提交时检测数据是否违反完整性。这种解决方案体现在CAS思想,即每一个线程修改时先去查询数据与之前去到的数据是否一致。但是这种做法有一个重大弊端:比如另一线程先修改了某个值,然后再改回原来值,这种情况下,CAS是无法判断的。