对象及变量的并发访问

本文详细探讨了Java中对象及变量的并发访问问题,重点关注`synchronized`关键字的使用,包括同步方法、同步代码块、锁重入、异常释放锁以及非继承性。同时,文章还分析了`volatile`关键字的作用和限制,以及在并发编程中如何确保线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


title: 对象及变量的并发访问
categories: Java多线程编程核心技术
tags: 多线程
time: 2019-04-21 08:36:04

synchronized同步方法

“非线程安全”会在多个线程中的同一对象的实例变量在并发访问时发生,产生的后果就是“脏读”,即读到的数据其实是被更改过的。
“线程安全”就是以获得的实例变量的值是经过同步处理过的,不会出现“脏读”现象。

方法内的变量为线程安全

“非线程安全”问题存在与“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,即线程安全。

实例变量非线程安全

如果多个线程访问一个对象的实例变量,则有可能产生“非线程安全”问题。

public class HasSelfPrivateNum {

    private int num = 0;


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

    public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        Test1 test1 = new Test1(hasSelfPrivateNum);
        Test2 test2 = new Test2(hasSelfPrivateNum);
        test1.start();
        test2.start();
    }

}

class Test1 extends Thread {
    private HasSelfPrivateNum hasSelfPrivateNum;
    Test1(HasSelfPrivateNum hasSelfPrivateNum) {
        this.hasSelfPrivateNum = hasSelfPrivateNum;
    }
    @Override
    public void run() {
        super.run();
        hasSelfPrivateNum.addNum("a");
    }
}

class Test2 extends Thread {
    private HasSelfPrivateNum hasSelfPrivateNum;
    Test2(HasSelfPrivateNum hasSelfPrivateNum) {
        this.hasSelfPrivateNum = hasSelfPrivateNum;
    }
    @Override
    public void run() {
        super.run();
        hasSelfPrivateNum.addNum("b");
    }
}

运行结果:

a set over
b set over
b num=200
a num=200

分析:Test1和Test2两个线程同时访问一个没有同步的方法addNum,则出现了线程不安全的问题。解决方式只需要给addNum方法前加synchronized关键字。

加锁后的执行效果:

a set over
a num=100
b set over
b num=200

多个对象多个锁

 public static void main(String[] args) {
        HasSelfPrivateNum hasSelfPrivateNum = new HasSelfPrivateNum();
        HasSelfPrivateNum hasSelfPrivateNum2 = new HasSelfPrivateNum();
        Test1 test1 = new Test1(hasSelfPrivateNum);
        Test2 test2 = new Test2(hasSelfPrivateNum2);
        test1.start();
        test2.start();
    }

运行结果:

a set over
b set over
b num=200
a num=100

分析:两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是异步的方式运行的。
关键字synchronized取得的锁是对象锁,而不是把一段代码或方法作为锁,所以在上面的示例中哪个线程先执行带synchrozined关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,例如上面示例中创建了2个HasSelfPrivateNum.java对象,所以就会产生2个锁,会异步执行。

synchronized方法与锁对象

验证synchronized与对象的关系

public class SynchronizedMethodLock {

    public void methodA() {
        try {
            System.out.println(Thread.currentThread().getName() + " run");
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SynchronizedMethodLock obj = new SynchronizedMethodLock();
        ThreadA threadA = new ThreadA(obj);
        threadA.setName("A");
        ThreadB threadB = new ThreadB(obj);
        threadB.setName("B");
        threadA.start();
        threadB.start();
    }

}

class ThreadA extends Thread {
    private SynchronizedMethodLock lock;

    public ThreadA(SynchronizedMethodLock obj) {
        this.lock = obj;
    }
    @Override
    public void run() {
        super.run();
        lock.methodA();
    }
}

class ThreadB extends Thread {
    private SynchronizedMethodLock lock;

    public ThreadB(SynchronizedMethodLock obj) {
        this.lock = obj;
    }
    @Override
    public void run() {
        super.run();
        lock.methodA();
    }
}

运行结果:

A run
B run
end
end

分析:给methodA不加锁的情况下,是异步执行的;

 synchronized public void methodA() {
        try {
            System.out.println(Thread.currentThread().getName() + " run");
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

给methodA加锁后是同步执行的,即排队执行。

A run
end
B run
end

结论:
1.调用synchronized声明的方法是排队运行的;
2.共享资源的读写需要同步化,否则就没有同步的必要。

同步方法和普通方法执行验证

public class SynchronizedMethodLock2 {

    synchronized public void methodA() {
        try {
            System.out.println(Thread.currentThread().getName() + " run:" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void methodB() {
        try {
            System.out.println(Thread.currentThread().getName() + " run:" + System.currentTimeMillis());
            Thread.sleep(5000);
            System.out.println("end");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        SynchronizedMethodLock2 obj = new SynchronizedMethodLock2();
        ThreadC threadC = new ThreadC(obj);
        threadC.setName("C");
        ThreadD threadD = new ThreadD(obj);
        threadD.setName("D");
        threadC.start();
        threadD.start();
    }

}

class ThreadC extends Thread {
    private SynchronizedMethodLock2 lock;

    public ThreadC(SynchronizedMethodLock2 obj) {
        this.lock = obj;
    }
    @Override
    public void run() {
        super.run();
        lock.methodA();
    }
}

class ThreadD extends Thread {
    private SynchronizedMethodLock2 lock;

    public ThreadD(SynchronizedMethodLock2 obj) {
        this.lock = obj;
    }
    @Override
    public void run() {
        super.run();
        lock.methodB();
    }
}

运行结果:

C run:1555810474080
D run:1555810474080
end
end

给methodB加锁后运行

C run:1555810535023
end
D run:1555810540026
end

结论:
1.ThreadA线程持有object锁,B线程可以以异步的方式调用object中的非synchronized类型的方法;
2.ThreadA线程持有object锁,B线程如果在这时调用object对象中的synchronized类型的方法则需要等待,也就是同步。

脏读

在赋值时虽然同步,但在取值时可能发生意想不到的意外,这种情况就是脏读(dirtyRead)。
发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

public class DirtyRead {

    private String username = "A";

    private String password = "AA";

    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;
            System.out.println("setValue:" + Thread.currentThread().getName()
                    + " username: " + username + " password: " + password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getValue() {
        System.out.println("getValue:" + Thread.currentThread().getName()
                + " username: " + username + " password: " + password);
    }
}

class ThreadF extends Thread {

    private DirtyRead dirtyRead;

    public ThreadF (DirtyRead dirtyRead) {
        this.dirtyRead = dirtyRead;
    }

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

    public static void main(String[] args) throws InterruptedException {
        DirtyRead dirtyRead = new DirtyRead();
        ThreadF threadF = new ThreadF(dirtyRead);
        threadF.start();
        Thread.sleep(200);
        dirtyRead.getValue();
    }
}

运行结果:

getValue:main username: B password: AA
setValue:Thread-0 username: B password: BB

分析:出现脏读的原因是因为getValue方法不是同步的,所以可以在任意时刻调用;
解决方法是添加synchronized关键字。

synchronized public void getValue() {
        System.out.println("getValue:" + Thread.currentThread().getName()
                + " username: " + username + " password: " + password);
    }

运行结果:OK!

setValue:Thread-0 username: B password: BB
getValue:main username: B password: BB

分析:setValue和getValue方法被依次执行。

结论:
1.当A线程调用对象中的synchronized方法X时,A线程就获取到了对象锁,其他线程必须等A执行完后才可以调用X方法,但B线程可以随意调用非synchronized方法。如果调用的是其他synchronized方法,也需要等待顺序执行;
2.脏读一定出现中操作实例变量的情况下,这也是不同线程“争抢”实例变量的结果。

synchronized锁重入

关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法的内部调用本类的其他synchronized方法时,是永远可以得到锁的。如果不可重入锁的话,就会造成死锁

”可重入锁“的概念:自己可以再次获取自己的内部锁。

public class SynLockIn {

    synchronized public void method1() {
        System.out.println("method1");
        method2();
    }

    synchronized public void method2() {
        System.out.println("method2");
        method3();
    }

    synchronized public void method3() {
        System.out.println("method3");

    }
}

class ThreadG extends Thread {
    SynLockIn synLockIn;

    public ThreadG(SynLockIn synLockIn) {
        this.synLockIn = synLockIn;
    }
    @Override
    public void run() {
        super.run();
        synLockIn.method1();
    }

    public static void main(String[] args) {
        SynLockIn synLockIn = new SynLockIn();
        ThreadG threadG = new ThreadG(synLockIn);
        threadG.start();
    }
}

运行结果:

method1
method2
method3

继承关系——重入锁

public class SynLockInExtends {

    public static void main(String[] args) {
        ThreadH threadH = new ThreadH();
        threadH.start();
    }
}

class ThreadH extends Thread{
    @Override
    public void run() {
        super.run();
        Sub sub = new Sub();
        sub.operateISubMethod();
    }
}

class Main {

    public int i = 10;

    synchronized public void operateIMainMethod() {
        try {
            i --;
            System.out.println("main 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 i = " + i);
                Thread.sleep(100);
                this.operateIMainMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

sub i = 9
main i = 8
sub i = 7
main i = 6
sub i = 5
main i = 4
sub i = 3
main i = 2
sub i = 1
main i = 0

结论:当存在继承关系是,子类可以通过“可重入锁”访问父类的同步方法。

出现异常,锁自动释放

当一个线程执行的代码出现异常时,其持有的锁会自动释放。

同步不具有继承性

同步不可以继承。即使父类的方法是synchronized,那么子类重写父类的方法想要同步效果,也要加synchronized关键字。

synchronized同步语句块

用关键字synchronized声明方法在某些情况下是有弊端的,例如A线程执行同步方法需要一个很长的时间,那么B线程则必须等待比较长的时间。在这样的情况下使用synchronized同步语句块来解决。synchronized方法是对当前对象加锁,而synchronized代码块是对某一个对象进行加锁。

synchronized方法的弊端

其他线程等待时间过长。

public class SynchronizedLongTime {

    private String getData1;

    private String getData2;

    synchronized public void doLongTimeTask(String data1, String data2) {
        try {
            System.out.println("begin task");
            System.out.println("线程:" + Thread.currentThread().getName());
            Thread.sleep(3000);
            // 线程安全主要在这儿!
            setData(data1, data2);
            System.out.println(Thread.currentThread().getName() + " getData1=" + getData1 + "; getData2=" + getData1);
            System.out.println("end task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void setData(String data1, String data2) {
        this.getData1 = data1;
        this.getData2 = data2;
    }

    public static void main(String[] args) {
        SynchronizedLongTime task = new SynchronizedLongTime();
        ThreadI threadI = new ThreadI(task);
        threadI.start();
        ThreadJ threadJ = new ThreadJ(task);
        threadJ.start();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long begin = CommonUtils.beginTime1;
        if(CommonUtils.beginTime1 > CommonUtils.beginTime2) {
            begin = CommonUtils.beginTime2;
        }
        long end = CommonUtils.endTime1;
        if (CommonUtils.endTime1 < CommonUtils.endTime2) {
            end = CommonUtils.endTime2;
        }
        System.out.println("总耗时:" + (end - begin)/1000 + " 秒");
    }
}

class ThreadI extends Thread {
    SynchronizedLongTime synchronizedLongTime;
    public ThreadI(SynchronizedLongTime synchronizedLongTime) {
        this.synchronizedLongTime = synchronizedLongTime;
    }
    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        synchronizedLongTime.doLongTimeTask("AAA", "aaa");
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}

class ThreadJ extends Thread {
    SynchronizedLongTime synchronizedLongTime;
    public ThreadJ(SynchronizedLongTime synchronizedLongTime) {
        this.synchronizedLongTime = synchronizedLongTime;
    }
    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        synchronizedLongTime.doLongTimeTask("BBB", "bbb");
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}

运行结果:

Connected to the target VM, address: '127.0.0.1:63459', transport: 'socket'
begin task
线程:Thread-0
Thread-0 getData1=AAA; getData2=AAA
end task
begin task
线程:Thread-1
Thread-1 getData1=BBB; getData2=BBB
end task
Disconnected from the target VM, address: '127.0.0.1:63459', transport: 'socket'
总耗时:6 秒

synchronized同步代码块的使用

当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内只能有一个线程被执行,另一个线程必须等待第一个线程执行完后方可被执行。

优化doLongTimeTask同步方法

public void doLongTimeTask(String data1, String data2) {
        try {
            System.out.println("begin task");
            System.out.println("线程:" + Thread.currentThread().getName());
            Thread.sleep(3000);
            // 线程安全主要在这儿!
            synchronized(this) {
                setData(data1, data2);
            }
            System.out.println(Thread.currentThread().getName() + " getData1=" + getData1 + "; getData2=" + getData1);
            System.out.println("end task");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
begin task
线程:Thread-0
begin task
线程:Thread-1
Thread-0 getData1=AAA; getData2=AAA
end task
Thread-1 getData1=BBB; getData2=BBB
end task
总耗时:3 秒

总结: 同步代码块比同步方法时间更少,运行效率更块。

一半同步,一半异步

不在synchronized中的代码异步执行,在synchronized中的是同步执行。

synchronized代码块间的同步性

在使用同步代码块的时候注意,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对同一个synchronized(this)同步代码块的访问将被阻塞,这说明synchronized使用的“对象监视器”是一个。

验证同步synchronized(this)代码块是锁定当前对象的

和synchronized方法一样,synchronized(this)代码块也是锁定当前对象的。

1.A线程调用同步代码块x,B线程调用非同步代码块y,则x和y是异步执行的;
2.A线程调用同步代码块x,B线程调用同步代码块z,则x和z是同步执行的,即按顺序执行。

将任意对象作为对象监视器

public class SynBlockString {

    private String username;
    private String password;
    private String string = new String();

    public void setUsernamePassword(String username, String password) {
        synchronized (string) {
            try {
                System.out.println(Thread.currentThread().getName() + " 在:" + System.currentTimeMillis() + " 进入同步块");
                this.password = password;
                Thread.sleep(3000);
                this.username = username;
                System.out.println(Thread.currentThread().getName() + " username: " + username + " password:" + password);
                System.out.println(Thread.currentThread().getName() + " 在:" + System.currentTimeMillis() + " 离开同步块");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        SynBlockString synBlockString = new SynBlockString();
        ThreadK threadK = new ThreadK(synBlockString);
        threadK.setName("线程A");
        threadK.start();
        ThreadL threadL = new ThreadL(synBlockString);
        threadL.setName("线程B");
        threadL.start();
    }
}

class ThreadK extends Thread{
    SynBlockString synBlockString;
    public ThreadK(SynBlockString synBlockString) {
        this.synBlockString = synBlockString;
    }
    @Override
    public void run() {
        super.run();
        synBlockString.setUsernamePassword("A", "a");
    }
}

class ThreadL extends Thread{
    SynBlockString synBlockString;
    public ThreadL(SynBlockString synBlockString) {
        this.synBlockString = synBlockString;
    }
    @Override
    public void run() {
        super.run();
        synBlockString.setUsernamePassword("B", "b");
    }
}

运行结果:

线程A 在:1555822223367 进入同步块
线程A username: A password:a
线程A 在:1555822226369 离开同步块
线程B 在:1555822226370 进入同步块
线程B username: B password:b
线程B 在:1555822229370 离开同步块

锁非this对象具有一定的优点:如果一个类中有多个synchronized方法,这时能实现同步,但会受到阻塞,所以影响效率;
但如果用同步代码块锁非this对象,则是异步的,不与其他锁this同步方法争抢this锁,则可大大提高效率。

修改代码

 public void setUsernamePassword(String username, String password) {
        String string = new String(); // 每次调用都产生一个新的String对象
        synchronized (string) {
            try {
                System.out.println(Thread.currentThread().getName() + " 在:" + System.currentTimeMillis() + " 进入同步块");
                this.password = password;
                Thread.sleep(3000);
                this.username = username;
                System.out.println(Thread.currentThread().getName() + " 在:" + System.currentTimeMillis() + " 离开同步块");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

执行结果:

线程A 在:1555822247681 进入同步块
线程B 在:1555822247683 进入同步块
线程A username: A password:a
线程A 在:1555822250682 离开同步块
线程B username: B password:b
线程B 在:1555822250685 离开同步块

分析:使用synchronized(非this对象x)同步代码块进行同步操作时,对象监视器必须是同一个对象。如果不是同一个对象监视器,运行的结果就是异步调用的,就会交叉执行。

public class SynList {

    private List<Integer> list = new ArrayList<Integer>();

    synchronized public void addA(Integer value) {
        System.out.println(Thread.currentThread().getName() + " 开始执行addA方法");
        list.add(value);
        System.out.println(Thread.currentThread().getName() + " 退出addA方法");
    }


    synchronized public void addB(Integer value) {
        System.out.println(Thread.currentThread().getName() + " 开始执行addB方法");
        list.add(value);
        System.out.println(Thread.currentThread().getName() + " 退出addB方法");
    }

    public static void main(String[] args) {
        SynList synList = new SynList();
        ThreadM threadM = new ThreadM(synList);
        threadM.setName("M");
        threadM.start();
        ThreadN threadN = new ThreadN(synList);
        threadN.setName("N");
        threadN.start();

    }
}

class ThreadM extends Thread {
    SynList synList = new SynList();
    public ThreadM(SynList synList) {
        this.synList = synList;
    }
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10000; i++) {
            synList.addA(i);
        }
    }
}

class ThreadN extends Thread {
    SynList synList = new SynList();
    public ThreadN(SynList synList) {
        this.synList = synList;
    }
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 10000; i++) {
            synList.addB(i);
        }
    }
}

运行结果:

省略...
M 退出addA方法
M 开始执行addA方法
M 退出addA方法
N 开始执行addA方法
N 退出addA方法
N 开始执行addA方法
N 退出addA方法
省略...
N 退出addA方法
N 开始执行addA方法
N 退出addA方法
M 开始执行addA方法
M 退出addA方法
省略...

分析:线程M和N异步执行了,可能会出现脏读的问题。

import java.util.ArrayList;
import java.util.List;

public class SynNotThisDirtyRead {
    public static void main(String[] args) {
        OneList oneList = new OneList();
        ThreadO threadO = new ThreadO(oneList);
        threadO.start();

        ThreadP threadp = new ThreadP(oneList);
        threadp.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("oneList.getSize = " + oneList.getSize());
    }
}

class OneList {
    private List<Integer> list = new ArrayList<Integer>();
    public void add(Integer value) {
        list.add(value);
    }

    public int getSize() {
        return list.size();
    }
}

class OneListService {
    synchronized public OneList addServiceMethod(OneList oneList, Integer value) {
        if (oneList.getSize() < 1) {
            try {
                // 模拟从远程获取数据的时间
                Thread.sleep(2000);
                oneList.add(value);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return oneList;
    }
}

class ThreadO extends Thread {

    private OneList oneList;

    public ThreadO(OneList oneList) {
        this.oneList = oneList;
    }
    @Override
    public void run() {
        super.run();
        OneListService service = new OneListService();
        service.addServiceMethod(oneList, 1);
    }
}

class ThreadP extends Thread {

    private OneList oneList;

    public ThreadP(OneList oneList) {
        this.oneList = oneList;
    }
    @Override
    public void run() {
        super.run();
        OneListService service = new OneListService();
        service.addServiceMethod(oneList, 1);
    }
}

执行结果:

oneList.getSize = 2

分析:“脏读”出现了,出现的原因是两个线程以异步的方式返回list参数的size大小。
解决办法是“同步化”。

修改OneListService.addServiceMethod方法

class OneListService {
    public OneList addServiceMethod(OneList oneList, Integer value) {
        synchronized (oneList) {
            if (oneList.getSize() < 1) {
                try {
                    // 模拟从远程获取数据的时间
                    Thread.sleep(2000);
                    oneList.add(value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return oneList;
    }
}

执行结果:

oneList.getSize = 1

分析:由于list参数对象在项目中是一份实例,是单例的,而且也正是对list参数的getSize方法做同步的调用,所以就对list参数进行同步处理。

细化验证3个结论

1.当多个线程同时执行synchronized(x){}同步代码块时呈同步效果
2.当其他线程执行x对象中的synchronized同步方法时呈同步效果
3.当其他线程执行x对象方法里面的synchronized(this)代码块时也呈同步效果

静态同步synchronized方法与synchronized(this)同步代码块

关键字synchronized用在static静态方法上,是对当前.java对应的class类持锁。
用在非static方法是给对象加锁。

两个线程分别调用同步的静态方法和普通方法,执行时异步的,因为静态同步方法的锁是给类加锁,而普通的同步方法是给对象加锁。

两个线程都调用同一个类静态同步方法,那他们是同步执行的,因为用的同一个类锁;

两个线程都调用同一个类的静态非同步方法,但方法中有同步代码块,那么也是同步执行的。即synchronized static == synchronized(this)

数据类型String的常量池特性

JVM中具有String常量池缓存的功能。

int a = "a“;
int b = "a";
a == b 为true

synchronized(string)和String联合使用。
两个线程同时调用synchronized(string)同步代码块,两个string的值时一样的,会出现两个线程拥有相同的锁,一个执行而另一个无法执行。这就是String常量池带来的问题。

解决方式:两个线程同时调用synchronized代码块,不用String作为锁对象,而是用Object作为锁对象,而Object并不会放入缓存中,每次new Object()得到的都是不同的对象,就不会出现上述问题了。

同步synchronized方法无限等待与解决

同步方法容易造成死循环。

可以用同步代码块的方式来解决。

多线程的死锁

不同线程都在等待一个永远无法释放的锁的线程,从而导致所有线程都无法完成任务。
避免双方互相持有对方的锁

jdk自带查看死锁工具
进入jdk安装目录下的bin目录, 执行jps

> jps
1111 run
> jstack -l 1111

内置类和静态内置类

内置类

public class A {
	class B {
		...
	}
}

静态内置类

public class A {
	static class B {
		...
	}
}

锁对象的改变

1.在将任何数据类型作为锁时,多个线程同时持有相同的锁,那线程之间是同步的,如果持有不同的锁,那线程是异步的。
2.如果一个对象做为锁,如果只是对象的属性变了,锁依然不会变,运行的结果依然是同步的。

volatile关键字

volatile关键字的作用是让变量在多个线程间可见。

volatile与死循环

volatile强行从公共堆栈中取得变量的值,而不是从线程私有的线程栈中取得变量值。

解决异步死循环

public class MyThread extends Thread {
	private  boolean isRunning= true;
	public void run() {
		while(isRunning) {
		}
		System.out.print("跳出循环");
	}
	
	public void setIsRunning(boolean isRunning) {
		this.isRunning= isRunning;
	}

	public static void main(String[] args) {
	    System.out.print("开始执行线程");
		MyThread my = new MyThread();
		my.start();
		my.setIsRunning(false);
		System.out.print("设置值为false");
	}
}

执行结果

开始执行线程

分析:让代码运行在JVM设置位Server服务器的环境中,即将JVM的参数设为-server,程序永远无法跳出循环,这是因为:变量isRunning存在与公共堆栈和线程的私有堆栈中,在JVM被设置为-server模式是为了线程的运行效率,线程一直在私有堆栈中取得isRunning的值为true,而代码my.setIsRunning(false)虽然执行,但只是将公共堆栈中的的isRunning设置为了true,并没有将私有堆栈中的修改,所以一直处于死循环的状态。

这个原因是由于公共堆栈与私有堆栈的数据不同步造成的,解决问题的方式就是使用volatile关键字,作用就是当使用isRunning变量时,强制从公共堆栈中取值。

使用volatile解决了线程间变量的可见性,但是volatile并不支持原子性。

synchronized和volatile的比较:
1.volatile是线程同步的轻量级实现,相比synchronized性能要好,并且volatile只能修饰变量,而synchronized可以修饰变量、方法、代码块。随着JDK新版本的发布,synchronized在运行效率上得到了很大的提升,用的地方也比较多。
2.多线程使用volatile不会出现阻塞,但synchronized会出现阻塞。
3.volatile可以保证变量的可见性,但不能保证原子性;synchronized既可以保证可见性,也可以保证原子性,因为它可以将私有内存和公共内存做同步。

线程安全围绕着可见性和原子性两个方面,确保线程安全。

volatile非原子的特性

虽然volatile增加了实例变量的可见性,但是不具备同步性,即原子性。
例如: i ++ 其实是非原子操作,①从内存中取出值i ②加1操作 ③将值赋给i。
如果在执行2⃣️的时候有一个线程修改了i的值,就会出现脏读数据。解决办法是加synchronized。

变量的内存工作过程:
1.read和load阶段:从内存复制变量到当前线程工作内存
2.use和assign阶段:执行代码,改变共享变量值
3.store和write阶段:用工作内存数据刷新主内存对应变量的值
在多线程环境中,use和assign是多次出现的,但这一操作不是原子性。

对于volatile变量,JVM只是保证从主内存中同步到线程工作内存的值是最新的,反之则不保证。

使用原子类进行i++实现

i++过程为了保证线程安全除用synchronized外,还可以用AtomicInteger原子类来实现。原子操作是不能分割的整体,没有其他线程能够中断或检查正在原子操作的变量。一个原子(Atomic)类型就是一个原子操作可用的类型,它可以在不加锁的情况下做到线程安全(thread-safe)。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerThread extends Thread{

     private AtomicInteger count = new AtomicInteger(0);

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

    public static void main(String[] args) {
        AtomicIntegerThread atomic = new AtomicIntegerThread();
        Thread t1 = new Thread(atomic);
        t1.start();
        Thread t2 = new Thread(atomic);
        t2.start();
        Thread t3 = new Thread(atomic);
        t3.start();
        Thread t4 = new Thread(atomic);
        t4.start();
    }
}

输出结果:

省略...
39995
39996
39997
39998
39999
40000

成功的累加到了5000

对比int++

public class AtomicIntegerThread extends Thread{
    private int count;
    public void run() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(count ++);
        }
    }

    public static void main(String[] args) {
        AtomicIntegerThread atomic = new AtomicIntegerThread();
        Thread t1 = new Thread(atomic);
        t1.start();
        Thread t2 = new Thread(atomic);
        t2.start();
        Thread t3 = new Thread(atomic);
        t3.start();
        Thread t4 = new Thread(atomic);
        t4.start();
    }
}

输出结果:

39992
39993
39994
39995
39996
39997
39998

最终结果为39998!!更预期的不一致(40000)

原子类也并不完全安全

原子类在具有逻辑的情况下输出结果也具有随机性。
原子类是原子的,但是一个方法如果有多个原子类的操作,是不能保证原子性的。


import java.util.concurrent.atomic.AtomicLong;

public class AtomicIntegerNoSafe {

    private AtomicLong aLong = new AtomicLong();

    public void add() {
        ;
        System.out.println("线程" + Thread.currentThread().getName() + " 加了100后的值是:" + aLong.addAndGet(100));
        System.out.println("线程" + Thread.currentThread().getName() + " 加了1后的值是:" + aLong.addAndGet(1));
    }

    public static void main(String[] args) {
        AtomicIntegerNoSafe noSafe = new AtomicIntegerNoSafe();
        ThreadQ[] threadQS = new ThreadQ[10];
        for (int i = 0; i < threadQS.length; i++) {
            threadQS[i] = new ThreadQ(noSafe);
        }
        for (ThreadQ threadQ : threadQS) {
            threadQ.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(noSafe.aLong.get());
    }
}

class ThreadQ extends Thread {
    AtomicIntegerNoSafe noSafe;
    public ThreadQ(AtomicIntegerNoSafe noSafe) {
        this.noSafe = noSafe;
    }
    @Override
    public void run() {
        super.run();
        noSafe.add();
    }
}

TODO: 方法add()不加synchronized也是顺序执行的,没看出来任何问题!!书中说是要加synchronized??

synchronized代码块有volatile同步的功能

关键字synchronized可以使多个线程访问同一资源具有同步性,而且它还具有将线程工作内存中的私有变量同步到公共内存中的功能。

关键字synchronized可以保证在同一时刻,只有一个线程可以执行某一个方法或一个代码块。它包含两个特性:可见性和互斥性(原子性)。同步synchronized不仅可以解决一个线程看到对象处于不一致的状态,还可以保证进入同步方法或者同步代码块的每个线程都看到由一个锁保护之前所有的修改效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值