多线程拾遗-2

时间:2023-02-15 23:40:49

Synchronize、volatile、lock相关

Synchronize修饰方法:

1,方法内变量不存在线程不安全问题,因为在内存模型中,每一个线程对应一个栈,而里方法里面的变量会在每个栈里有一个,不存在互相干扰的问题。
2,实例变量非线程安全。其实就是,当不同的线程对同一块内存上的对象进行操作时,因为cpu的随机执行,可能会出现a线程执行到一半被b线程抢占了cpu,导致b拿到的不是最新的状态,或者b线程的操作又被后来的a操作覆盖。
因此,当多个线程访问同一个对象时,需要加上synchronize关键字来修饰该对象,表示当某线程进入synchronize修饰的方法时,其他线程必须等待该线程执行完成,才可以继续执行同样被synchronize修饰的方法。
需要注意,必须是synchronize修饰的同一个对象,才会发生某线程等待的情况。如果a线程占有的objectA锁而b线程占有的是objecyB锁,他们直接会是异步执行。是否锁的同一个对象可以通过:
1, 是否synchronize()中是同一个对象来判断。
2, 当修饰方法时,this是不是同一个东西来判断。

Synchronize是可重入锁:

public class SynchronizedRelock {

    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");
    }

    public static void main(String[] args) {
        new TestThread(new SynchronizedRelock()).start();
    }
}

class TestThread extends Thread{

    private SynchronizedRelock synchronizedRelock;

    public TestThread(SynchronizedRelock synchronizedRelock){
        this.synchronizedRelock = synchronizedRelock;
    }

    @Override
    public void run(){
        synchronizedRelock.service1();
    }
}

可以看到,尽管该线程在service1()中已经锁住了this对象,即new synchronizeRelock()实例,但是依然是可以访问service2()和service3()。即可重入锁是可以*访问任何自己已经取得锁的地方。

一个例子:

class FatherClass{
    synchronized public void fatherService(){
        System.out.println("father menthod");
    }
}

class SonClass extends FatherClass{
    synchronized public void sonService(){
        System.out.println("son methof");
        fatherService();
    }
}
public class SynchronizedRelock {

    public static void main(String[] args) {
        new TestThread(new SonClass()).start();
    }
}

class TestThread extends Thread{
    private SonClass sonClass;
    public TestThread(SonClass sonClass){
        this.sonClass = sonClass;
    }

    @Override
    public void run(){
        sonClass.sonService();
    }
}

可以看到,子类在锁定一个对象锁时,通过对象调用父类方法也是可以的,因为父类的方法锁定的也是该子类对象。但是要注意,父类和子类均需要用synchronize来修饰,synchronize关键字并不能继承。

Synchronize修饰代码块:

Synchronize修饰方法有一个弊端,就是当方法体内除了锁对象的改变,还有许多大量的非所对象的操作,由于synchronize修饰了整个方法,会导致效率低下,这个时候可以考虑使用synchronize修饰代码块来代替。
比如:

synchronize public void test(){
        Thread.sleep(10000);
        This.change();  
    }

当有两个线程同时访问该方法时,会导致他们同步执行,也就是说两个线程分别sleep 10000毫秒。
改为:

    public void test(){
    Thread.sleep(10000);
synchronized(this){
    this.change();
}
} 

这样,就可以保证两个线程异步sleep,效率提升了一倍。

脏读问题:

public class DirtyR{
    private int i;

    synchronized public void add(){
        System.out.println(i++);
    }

    synchronized public void sub(){
        System.out.println(i--);
    }

    public static void main(String[] args) {
        DirtyR dirtyR = new DirtyR();
        new AddThread(dirtyR).start();
        new SubThread(dirtyR).start();
    }
}

class AddThread extends Thread{
    DirtyR dirtyR;
    public AddThread(DirtyR dirtyR){
        this.dirtyR = dirtyR;
    }

    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            dirtyR.add();
        }
    }

}

class SubThread extends Thread{
    DirtyR dirtyr;
    public SubThread(DirtyR dirtyR){
        this.dirtyr = dirtyR;
    }

    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            dirtyr.sub();
        }
    }
}

运行该代码,虽然没有出现脏读的现象,但可以虽然用了synchronize来修饰,但是并不是按照加到100再减到1的顺序执行,这就为脏读提供了环境。

静态同步synchronized方法和synchronized(class)代码块

要分清楚静态方法锁的是类本身,即在内存中代表该类的一个class对象。即使使用实例来调用静态方法,也是会锁住类本身。即当a实例和b实例同时调用被synchronized修饰的静态方式时,也是被同步执行的。

public class DirtyR{
    private static int i;

    synchronized public static void add(){
        System.out.println(i++);
    }


    public static void main(String[] args) {
        DirtyR dirtyR1 = new DirtyR();
        DirtyR dirtyR2 = new DirtyR();
        new AddThread(dirtyR1).start();
        new AddThread(dirtyR2).start();
    }
}

class AddThread extends Thread{
    DirtyR dirtyR;
    public AddThread(DirtyR dirtyR){
        this.dirtyR = dirtyR;
    }

    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            dirtyR.add();
        }
    }

}

class SubThread extends Thread{
    DirtyR dirtyr;
    public SubThread(DirtyR dirtyR){
        this.dirtyr = dirtyR;
    }

    @Override
    public void run(){
        for (int i = 0; i < 100; i++) {
            dirtyr.add();
        }
    }
}

可以看到,最终的输出也是同步的。

用String作为锁时的常量池问题。

public class DirtyR{
    private int i;
    String lock1 = "123";
    String lock2 = "123";
    public void add1(){
        synchronized (lock1){
            for (int j = 0; j < 100; j++) {
                System.out.println(Thread.currentThread()+" "+i++);
            }
        }
    }

    public void add2() throws InterruptedException {
        synchronized (lock2){
            for (int j = 0; j < 100; j++) {
                System.out.println(Thread.currentThread()+" "+i+++" "+j);
            }
        }
    }


    public static void main(String[] args) {
        DirtyR dirtyR1 = new DirtyR();
        new AddThread(dirtyR1).start();
        new SubThread(dirtyR1).start();
    }
}

class AddThread extends Thread{
    DirtyR dirtyR;
    public AddThread(DirtyR dirtyR){
        this.dirtyR = dirtyR;
    }

    @Override
    public void run(){
        dirtyR.add1();
    }

}

class SubThread extends Thread{
    DirtyR dirtyr;
    public SubThread(DirtyR dirtyR){
        this.dirtyr = dirtyR;
    }

    @Override
    public void run(){
        try {
            dirtyr.add2();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可以看到,虽然两个方法锁的对象时lock1和lock2,但由于常量池的存在,lock1和lock2实际上是同一个对象,随意最终还是以同步的方法在执行。可以用Object对象来作为
死锁,即双方都在等在对方释放锁后自己释放锁。

当锁对象发生改变时:

public class DirtyR{
    private int i;
    String s = "lock";
    public void add1() throws InterruptedException {
        synchronized(s){
            s="newlock";
            Thread.sleep(50);
            for (int j = 0; j < 100; j++) {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public void add2(){
        synchronized(s){
            System.out.println(s);
            for (int j = 0; j < 100; j++) {
                System.out.println(Thread.currentThread().getName());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DirtyR dirtyR1 = new DirtyR();
        new AddThread(dirtyR1).start();
        Thread.sleep(50);
        new SubThread(dirtyR1).start();
    }
}

class AddThread extends Thread{

    private DirtyR dirtyR;

    public AddThread(DirtyR dirtyR){
        this.dirtyR = dirtyR;
    }

    @Override
    public void run(){
        try {
            dirtyR.add1();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class SubThread extends Thread{

    private DirtyR dirtyr;

    public SubThread(DirtyR dirtyR){
        this.dirtyr = dirtyR;
    }

    @Override
    public void run(){
        dirtyr.add2();
    }

}

可以看到当lock对象发生改变时,便会异步执行。

volatile关键字 (等有时间专门写volatile的东西)

volatile的主要作用是,保证线程的对某变量的读取总是从公共内存中读取的,增加了实例变量在多个线程间的可见性。
和synchronized比较,volatile更多的是增加变量在线程间的可见性,销量会高过synchronized,但是synchronized可以解决线程间的同步问题,也保证原子性。
原子类也可以保证原子性,却不可以保证代码间的执行顺序,因此synchronized还是最保险的措施。