二、对象及变量的并发访问
只有共享资源的多线程访问,为了避免幻读(读到已经更新的数据),才需要考虑同步。
1.共享实例变量是线程不安全的
public class SelPrivateNum {
private int num = 0;
// 这个方法由于num被使用 , 是线程不安全的
public void addI(String chose){
if(chose.equals("a")){
num = 100;
System.out.println(" a set over");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
num = 200;
System.out.println(" b set over");
}
System.out.println( chose + " num = " + num );
}
}
public class ThreadA extends Thread {
private SelPrivateNum numRef;
public ThreadA(SelPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
public class ThreadB extends Thread{
private SelPrivateNum numRef;
public ThreadB(SelPrivateNum numRef) {
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
public static void main(String[] args) {
SelPrivateNum selPrivateNum = new SelPrivateNum();
ThreadA threadA = new ThreadA(selPrivateNum);
ThreadB threadB = new ThreadB(selPrivateNum);
threadA.start();
threadB.start();
}
输出结果如下: 预期 a=100 ,但是结果 是 =200 不符合预期
a set over
b set over
b num = 200
a num = 200
2. 解决办法是在会被多线程使用的方法加上 synchronized 关键字
同步方法所使用的锁都是对象锁,也就是对于使用同一个实例的多线程有效
ublic class SelPrivateNum {
private int num = 0;
public synchronized void addI(String chose){
if(chose.equals("a")){
num = 100;
System.out.println(" a set over");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
num = 200;
System.out.println(" b set over");
}
System.out.println( chose + " num = " + num );
}
}
输出结果 : 符合预期。
b set over
b num = 200
a set over
a num = 100
分析:当 线程A或B 开始执行这个同步方法时,另一个线程必须等 前一个线程使用完毕同步方法之后才能执行这个同步方法。因此就保证了在这个方法内,执行结果不会受到其他线程干扰。
3. 脏读
脏读的产生,一定是对同一个实例变量,在多个线程之间异步操作产生。
//伪代码:
class DataPo{
private T val;
public synchronized void set(T val) ;
public synchronized void get();
}
场景分析:如果有两个线程同时使用 DataPo的同一个实例,一个线程使用设置值的方法,另一个使用取出值的方法。
不加synchronized的时候:
那么由于两个方法异步而且操作的是同一实例变量,就会导致值还没有被正确的设置(只设置到一半),就被另一个线程取出来使用了,这样读到的数据虽然是被修改过的,但是却不是被完整的修改的,变成脏读。
两个方法都加上synchronized:
由于两个方法使用的是同一个锁(实例本身的对象锁),所以虽然两个线程在分布调用同一个实例的两个同步方法,这两个同步方法也会顺序排队执行
4. synchronized是可重入锁
一个对象实例内部有多个同步方法,这些同步方法是可以互相调用的,因为synchronized是实现了可重入锁, 。 在调用链上,外层方法获得锁之后,其内层方法可以同样获得这个锁,这样就称为可重入锁。
子类对象的同步方法内部同样可以调用父类同步方法。
线程抛出异常时,其持有的锁会释放
5.使用同步代码块优化效率
public synchronized void methodA(){
Thread.sleep(5000);
doRealThings;
}
public void methodB(){
Thread.sleep(5000);
synchronized(this){
doRealThings;
}
}
真正需要保证线程安全同步执行的代码可能只是 doRealThings 里面的代码,那么其实不需要让整个方法都同步,而使用同步代码块,就可以让这个方法中的其他代码异步,而同步代码块内部的同步执行,提高效率。
同一时间,同一实例,只有一个线程可以执行synchronized中的代码
需要注意的是,同一实例的各个同步代码块之间也是需要同步执行的,跟同步方法之间的关系类似。并且同步代码块跟同步方法之间也是需要同步执行。
6. 使用不同的对象监视器
实际上 同步方法 等同于 synchronized(this){ },它们的 对象监视器都是 this实例。只要是对象监视器相同的 同步代码直接,就会互相阻塞排队执行。
因此, synchronized( anyObj ) 和 synchronized( this) 之间的同步代码之间是互相独立的,因为对象监视器不同。
考虑线程是否安全的思想:
如果一个多线程共享变量会变化,那么在使用到这个共享变量的地方,尤其是使用来做运算的时候,需要考虑线程安全问题,解决方式就可以是同步化。
使用 synchronized 关键字的方法 所用到的变量都是从主内存中读取的,等同于在 list 前面加上 volatile,不论这个变量是否包含在 synchronized代码块中,如下:
public class ThreadA extends Thread {
private MyList list;
public ThreadA(MyList list) {
this.list = list;
}
@Override
public void run() {
super.run();
try {
while (true) {
synchronized (this){
// 这个synchronized只是保证变量从主存读取
}
// 这个 list 是从主内存中取的
if (list.size() == 5) {
System.out.println("线程A退出");
throw new InterruptedException();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7. 以一个对象实例作为锁对象时
会与这个对象实例内部的 synchronized 方法 或synchronized(this)代码块,同步执行。
class Object{
public synchronized void methodA(){}
}
class Task{
public void doTask(Object obj){
synchronized(obj){
doSomething();
}
}
}
class App{
public void main(String[] args){
Object obj = new Object();
Task task = new Task();
// 这两个方法将会同步执行,因为他们使用了同一个锁对象
task.doTask( obj );
obj.methodA();
}
}
8. synchronized static方法 等同于 synchronized ( this.class ){ }
实际上,使用 synchronized 方法有风险,因为我们的目的只是为了保证 单一一个方法 不允许多线程并行,但是 同步方法却造成了,只要这个对象是同一个,里面的多个同步方法都不能多线程并行。 如果出现了其中摸一个方法的阻塞,或者死锁,就会导致这个对象的其它所有同步方法不能使用。
解决办法是对每个需要同步的独立方法使用 synchronized(对象),这样既保证了指定方法不能并行,又保证了各个方法之间不受干扰。
所以 同步方法 适用于需要保证对象内各同步方法执行顺序的情况下。
class Test{
Object obj1 = new Object();
Object obj2 = new Object();
public void test1{
synchronized( obj1 ){
}
}
public void test2{
synchronized( obj2 ){
}
}
}
9.锁对象变动的影响
-
jdk常量池中的对象,比如 String 类型 ”AA“ 与另一个 ”AA“ 两者被认为是同一个对象,在对象锁的应用中也是认为是同一个对象的。
-
synchronized( obj ) 中的对象可以动态的替换,这样就会异步了
-
sybchronized( obj ) 中 0bj 中的属性变化时,其锁对象仍然不会受影响
10. volatile 关键字保证多线程共享资源可见性
子级线程使用到 实例变量或类变量 时,会优先从私有堆栈中取值,如果更新了这个实例变量,是放在自己的私有堆栈 和 父级的堆栈中的(公共堆栈);同理,各个子级线程内部的对实例变量的更新也是发生在各自私有堆栈和公共堆栈 中,对其它子级线程不可见。
父级线程使用 实例变量或类变量 时,使用的是自己的私有堆栈(是自身和各级子线程的公共堆栈)。
使用 volatile 关键字修饰变量,保证各个使用到此变量的线程都从 公共堆栈取值,保证了变量在各线程间的一致性。
public class ThreadC extends Thread{
private volatile BoolWrapper tag = new BoolWrapper(true);
public boolean isTag() {
return tag.isTag();
}
public void setTag(boolean tag) {
this.tag.setTag(tag);
}
@Override
public void run() {
super.run();
System.out.println("进入run了");
while (isTag()){
}
System.out.println("线程被停止了");
}
}
public class TestC {
public static void main(String[] args) throws InterruptedException {
final ThreadC thread1 = new ThreadC();
thread1.start();
Thread.sleep(1000);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
thread1.setTag(false);
System.out.println("子线程2已赋值为false");
}
});
thread2.start();
Thread.sleep(1000);
System.out.println("父级线程中显示第一次:" + thread1.isTag());
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
thread1.setTag(true);
System.out.println("子线程3已赋值为true");
}
});
thread3.start();
Thread.sleep(1000);
System.out.println("父级线程中显示第二次:" + thread1.isTag());
}
}
11. volatile 的不足—对 volatile 变量操作的是非原子性的
为了保证多条操作为原子性,则需要使用 synchronized 关键字,不用volatile关键字。
也就是说,在对变量 只读不写 的场景下使用volatile合理,在需要写入的情况下,就需要使用synchronized。
public class ThreadD extends Thread{
volatile private static int count;
private static void addCount(){
for (int i = 0; i < 100 ; i++) {
count++;
}
}
}
其中 count++ ;这个操作分解为
- 从内存中取出 count ; — volatile 从主内存取值
- 计算 count+1 的值;
- 将 count 写回内存; — volatile 将结果写回线程工作内存并同步到主内存
场景:
线程 A 在步骤1 取得 count = 10;
同时,线程B 在步骤1取得count=10;
线程A 执行步骤 2,结果 count= 11;
线程B 执行步骤 2,同样结果 count= 11;
线程A 执行步骤 3,将结果 count= 11写回内存;
线程B 执行步骤 3,将结果 count= 11写回内存;
这样 事实上经过了两次计算,count 的预期结果应该为 12,但是实际上结果为11,这样的现象就是由于非原子操作产生了脏读现象。(读到的值是另一个线程操作之前的值)
12.原子类进行运算可以保证原子性,保证结果正确,但不保证顺序性
public class ThreadTest extends Thread{
private AtomicInteger count = new AtomicInteger( 0 );
@Override
public void run(){
System.out.println( Thread.currentThread().getName()+"加了100后的值 是"+count.addAndGet(100));
// 这里两个原子操作,本身就组成了一个两步骤的非原子操作了
count.addAndGet(1);
}
}
以上代码如果有多个线程同时执行,最后的对 count的值的计算结果保证正确,但是在 过程中输出打印出的结果具有随机性。