Java高并发编程——常见问题、举例(3)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a_hui_tai_lang/article/details/79528488

1、深入理解synchronized关键字
synchronized锁住的是对象
  你是怎么理解synchronized的呢?看看以下的一段代码,synchronized锁住的是花括号里面的代码吗?其实synchronized锁住的是一个对象(即以下代码的o对象)。

public class T {
    private int count = 10;
    private Object o = new Object();

    public void m() {
        synchronized(o) { //任何线程要执行下面的代码,必须先拿到o的锁
            count--;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}

  再看一段代码,继续理解和体会锁住对象的含义

public class T {

    private int count = 10;

    public void m() {
        synchronized(this) { //任何线程要执行下面的代码,必须先拿到this的锁
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
        }
    }
}
public class T {

    private int count = 10;
    public synchronized void m() { //等同于在方法的代码执行时要synchronized(this)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
}

2、静态方法的锁

public class T {
    private static int count = 10;
    public synchronized static void m() { //这里等同于synchronized(T.class)
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }

    public static void mm() {
        synchronized(T.class) { //考虑一下这里写synchronized(this)是否可以?
            count --;
        }
    }
}

  考虑一下这里写synchronized(this)是否可以?答案是不可以的,不信可以用开发软件试一下,不用运行就已经提示你代码有问题。原因是静态方法一开始就加载了,而对象还没有new出来,自然也不能用synchronized关键字来锁对象,而只能锁这个类对象(T.class)。
3、线程安全问题

public class T implements Runnable {
    private int count = 10;
    public /*synchronized*/ void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void main(String[] args) {
        T t = new T();
        for(int i=0; i<5; i++) {
            new Thread(t, "THREAD" + i).start();
        }
    }
}

  以上代码如果没有加synchronized关键字,则有可能出现线程安全的问题,比如结果:
THREAD0 count = 9
THREAD2 count = 7
THREAD3 count = 7
THREAD1 count = 6
THREAD4 count = 5
  自己思考一下,自然能够想明白,再看看以下代码暗藏的陷阱,猜猜结果是多少

public class T implements Runnable {
    private int count = 10;
    public synchronized void run() { 
        count--;
        System.out.println(Thread.currentThread().getName() + " count = " + count);
    }
    public static void main(String[] args) {

        for(int i=0; i<5; i++) {
            T t = new T();
            new Thread(t, "THREAD" + i).start();
        }
    }
}

  程序输出结果为:
THREAD2 count = 9
THREAD1 count = 9
THREAD0 count = 9
THREAD4 count = 9
THREAD3 count = 9
是不是很惊喜,为什么是这个结果呢?仔细观察T t=new T()的位置即可。

4、同步和非同步方法是否可以同时调用?
当然是可以的,以下代码为证

public class T {
    public synchronized void m1() { 
        System.out.println(Thread.currentThread().getName() + " m1 start...");
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m1 end");
    }
    public void m2() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " m2 ");
    }
    public static void main(String[] args) {
        T t = new T();
        /*new Thread(()->t.m1(), "t1").start();
        new Thread(()->t.m2(), "t2").start();*/

        new Thread(t::m1, "t1").start();
        new Thread(t::m2, "t2").start();
    }
}

结果:
t1 m1 start…
t2 m2
t1 m1 end
  分析:m1方法是一个同步方法(带有synchronized关键字),程序开始打印“t1 m1 start…”睡10秒钟,程序结束打印“t1 m1 end”;m2方法是一个非同步方法,睡5秒钟打印t2 m2 。两个线程同时跑,由结果可以看出,m1跑的同时,m2方法也能运行。
5、脏读现象
对业务写方法加锁
对业务读方法不加锁
容易产生脏读问题(dirtyRead)

import java.util.concurrent.TimeUnit;

public class Account {
    String name;
    double balance;
    public synchronized void set(String name, double balance) {
        this.name = name;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.balance = balance;
    }
    public /*synchronized*/ double getBalance() {
        return this.balance;
    }
    public static void main(String[] args) {
        Account2 a = new Account2();
        new Thread(()->a.set("zhangsan", 100.0)).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance());
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(a.getBalance());
    }
}

输出结果为:
0.0
100.0
这里发生了脏读现象,如果在读方法(getBalance)处使用synchronized关键字,输出结果变为:
100.0
100.0

6、可重入锁
  当一个线程要获取一个被其他线程占用的锁时候,该线程会被阻塞,那么当一个线程再次获取它自己已经获取的锁时候是否会被阻塞那?如果不需要阻塞那么我们说该锁是可重入锁,也就是说只要该线程获取了该锁,那么可以无限制次数进入被该锁锁住的代码。

  先看一个例子如果锁不是可重入的,看看会出现什么问题。

public class Hello{
     public Synchronized void helloA(){
        System.out.println("hello");
     }

     public Synchronized void helloB(){
        System.out.println("hello B");
        helloA();
     }

}

  如上面代码当调用helloB函数前会先获取内置锁,然后打印输出,然后调用helloA方法,调用前会先去获取内置锁,如果内置锁不是可重入的那么该调用就会导致死锁了,因为线程持有并等待了锁。

  实际上内部锁是可重入锁,例如synchronized关键字管理的方法,可重入锁的原理是在锁内部维护了一个线程标示,标示该锁目前被那个线程占用,然后关联一个计数器,一开始计数器值为0,说明该锁没有被任何线程占用,当一个线程获取了该锁,计数器会变成1,其他线程在获取该锁时候发现锁的所有者不是自己所以被阻塞,但是当获取该锁的线程再次获取锁时候发现锁拥有者是自己会把计数器值+1, 当释放锁后计数器会-1,当计数器为0时候,锁里面的线程标示重置为null,这时候阻塞的线程会获取被唤醒来获取该锁。
  再看一段代码

import java.util.concurrent.TimeUnit;

public class T {
    synchronized void m() {
        System.out.println("m start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m end");
    }
    public static void main(String[] args) {
        new TT().m();
    }
}

class TT extends T {
    @Override
    synchronized void m() {
        System.out.println("child m start");
        super.m();
        System.out.println("child m end");
    }
}

  一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁。也就是说synchronized获得的锁是可重入的这里是继承中有可能发生的情形,子类调用父类的同步方法
7、释放锁
  程序在执行过程中,如果出现异常,默认情况锁会被释放。所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常


import java.util.concurrent.TimeUnit;

public class T {
    int count = 0;
    synchronized void m() {
        System.out.println(Thread.currentThread().getName() + " start");
        while(true) {
            count ++;
            System.out.println(Thread.currentThread().getName() + " count = " + count);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(count == 5) {
                int i = 1/0; //此处抛出异常,锁将被释放,要想不被释放,可以在这里进行catch,然后让循环继续
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        Runnable r = new Runnable() {

            @Override
            public void run() {
                t.m();
            }
        };
        new Thread(r, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(r, "t2").start();
    }
}

输出结果:
t1 start
t1 count = 1
t1 count = 2
t1 count = 3
t1 count = 4
t1 count = 5
t2 start
t2 count = 6
Exception in thread “t1” java.lang.ArithmeticException: / by zero
at yxxy.c_011.T.m(T.java:28)
at yxxy.c_011.T$1.run(T.java:39)
at java.lang.Thread.run(Thread.java:745)
t2 count = 7
t2 count = 8

  分析:m()方法是同步方法,作用是不停的输出1,2,3…,当count=5时,会抛一个异常。一开始,第一个线程t1启动,打印,由于m()方法是个同步方法,synchronized上锁,锁住t对象,t2线程阻塞等待直到t对象的锁释放掉;当count=5时,抛出异常,t1线程挂了,锁被释放掉了,t2线程就启动了,由此得到了打印结果。
8、volatile关键字
   volatile 关键字,使一个变量在多个线程间可见, A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。使用volatile关键字,会让所有线程都会读到变量的修改值。
  在下面的代码中,running是存在于堆内存的t对象中。当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修running的值之后,t1线程感知不到,所以不会停止运行。使用volatile,将会强制所有线程都去堆内存中读取running的值。
 可以阅读这篇文章进行更深入的理解http://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
  volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

import java.util.concurrent.TimeUnit;

public class T {
    /*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
    void m() {
        System.out.println("m start");
        while(running) {
            /*
            try {
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        T t = new T();

        new Thread(t::m, "t1").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running = false;
    }
}

  如果没有volatile关键字的话,输出结果应该是只有m start,然后程序就进入一个什么也不做的死循环;如果有volatile的话,输出结果应该是
m start
m end!
  因为volatile保证了数据的可见性,但是这个程序自己运行的时候,不管有没有volatile关键字,都是上面的结果,我也不知道是怎么回事,难道是jvm和以前的不一样了?
9、volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized,运行下面的程序,并分析结果

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

public class T {
    volatile int count = 0; 
    void m() {
        for(int i=0; i<10000; i++) count++;
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<Thread>();
        for(int i=0; i<10; i++) {
            threads.add(new Thread(t::m, "thread-"+i));
        }
        threads.forEach((o)->o.start());
        threads.forEach((o)->{
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

输出的结果是一个随机的数字
对比上一个程序,可以用synchronized解决,synchronized可以保证可见性和原子性,volatile只能保证可见性

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

public class T {
    /*volatile*/ int count = 0;
    synchronized void m() { 
        for (int i = 0; i < 10000; i++)
            count++;
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());
        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

  输出结果为100000
  这两个例子充分说明了synchronized可以保证可见性和原子性,volatile只能保证可见性
10、原子性
  解决同样的问题的更高效的方法,使用AtomXXX类,AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T {
    /*volatile*/ //int count = 0;

    AtomicInteger count = new AtomicInteger(0); 

    /*synchronized*/ void m() { 
        for (int i = 0; i < 10000; i++)
            //if count.get() < 1000
            count.incrementAndGet(); //count++
    }

    public static void main(String[] args) {
        T t = new T();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }
        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(t.count);
    }
}

  输出结果:100000
  分析的话,原子性嘛,很好理解
11、synchronized优化

import java.util.concurrent.TimeUnit;
public class T {
    int count = 0;
    synchronized void m1() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        count ++;
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    void m2() {
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //业务逻辑中只有下面这句需要sync,这时不应该给整个方法上锁
        //采用细粒度的锁,可以使线程争用时间变短,从而提高效率
        synchronized(this) {
            count ++;
        }
        //do sth need not sync
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

12、避免将锁定对象的引用变成另外的对象

import java.util.concurrent.TimeUnit;

public class T {
    Object o = new Object();
    void m() {
        synchronized(o) {
            while(true) {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName());           
            }
        }
    }

    public static void main(String[] args) {
        T t = new T();
        //启动第一个线程
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //创建第二个线程
        Thread t2 = new Thread(t::m, "t2");
        t.o = new Object(); //锁对象发生改变,所以t2线程得以执行,如果注释掉这句话,线程2将永远得不到执行机会
        t2.start();

    }
}

输出结果为:
t1
t1
t1
t1
t2
t1
13、不要以字符串常量作为锁定对象
   在下面的例子中,m1和m2其实锁定的是同一个对象,这种情况还会发生比较诡异的现象,比如你用到了一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了”Hello”,这时候就有可能发生非常诡异的死锁阻塞,因为你的程序和你用到的类库不经意间使用了同一把锁

public class T {

    String s1 = "Hello";
    String s2 = "Hello";

    void m1() {
        synchronized(s1) {

        }
    }

    void m2() {
        synchronized(s2) {

        }
    }

}

猜你喜欢

转载自blog.csdn.net/a_hui_tai_lang/article/details/79528488
今日推荐