多线程同步问题

先看Demo1:

public class Demo1_Synchronized {
    public static void main(String[] args) {
        final Printer p = new Printer();
        new Thread() {
            public void run() {
                for (int i = 0; i < 100; ++i) {
                    p.print1(); // jdk1.8不需要显式将p用final修饰,默认用final修饰
                }
            }
        }.start();
        new Thread() {
            public void run() {
                for (int i = 0; i < 100; ++i) {
                    p.print2();
                }
            }
        }.start();
    }
}

class Printer {
    // 零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:
    // 生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
    private byte[] lock = new byte[0]; // 特殊的锁
    // 注意:匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象
    // 非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果
    // 静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的

    public static void print1() {
        // synchronized (lock) {
        synchronized (Printer.class) {
            System.out.print("我");
            System.out.print("最");
            System.out.print("帅");
            System.out.print("\r\n");
        }
        // }
    }

    public synchronized static void print2() {
        // synchronized (lock) {
        System.out.print("你");
        System.out.print("很");
        System.out.print("丑");
        System.out.print("\r\n");
        // }
    }
}

注意:做好不要把匿名对象不可以当做锁对象,因为不能保证两个锁对象是同一个对象,这样就只能锁住类了。

非静态的同步方法,锁对象是this,锁方法和锁this是一样的效果

比如public synchronized void print(){...}

就和public void print(){

     synchronized(this){...}

}

效果是一样的。

静态的同步方法,锁对象是当前类的字节码对象,锁方法和锁Printer.class是一样的,相当于给当前的类加锁

public synchronized static void print(){...}

和public static void print(){

    synchronized(Printer.class){...}// 类名.class

}

效果是一样的。

经验总结:

如果是多个线程指向相同的对象,那么多个线程操作这个共享变量时,这个变量是不需要加static的,锁住当前this即可保证线程安全。如下:

public class MainClass implements Runnable {
    static MainClass instance = new MainClass();
    volatile int i = 0;

    public synchronized void increase() {
        ++i;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for (int i = 0; i < 10000; ++i) {
            increase();
        }
        System.out.println(i);
    }
}

 

如果是多个线程指向不同的对象,那么多个线程操作这个共享变量时,这个变量是需要加上static的,操作这个变量的方法必须设置为static方法才能操作这个变量,再锁住这个静态方法,就是锁住了这个类,可以正确同步,保证线程安全。如下:

public class MainClass implements Runnable {
    static MainClass instance = new MainClass();
    static volatile int i = 0;

    public static synchronized void increase() {
        ++i;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new MainClass());
        Thread t2 = new Thread(new MainClass());
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }

    @Override
    public void run() {
        // TODO Auto-generated method stub
        for (int i = 0; i < 10000; ++i) {
            increase();
        }
        System.out.println(i);
    }
}

 

来看下一个Demo2

public class Demo2_Ticket {
    /**
     * @param args
     *            模拟铁路售票, 4个窗口卖100张票
     */
    public static void main(String[] args) {
        for (int i = 0; i < 4; ++i) {
            new TicketSeller("窗口" + i).start();
        }
    }
}

class TicketSeller extends Thread {
    private static int tickets = 100;

    public TicketSeller(String name) {
        super(name);
        // TODO Auto-generated constructor stub
    }

    public void run() {
        while (true) {
            synchronized (TicketSeller.class) { // ==========提问点提问点提问点======
                if (tickets == 0) {
                    break;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(getName() + "...这是第" + tickets-- + "号票");
            }
        }
    }
}

模拟铁路卖票100张,肯定不能重复卖票,不然上车该争车位发生矛盾了。

看到提问点

如果这里不加synchronized,可以吗?

    不可以,如果不加,可能卖出负数号票,比如线程3刚要进行下一次循环,此时tickets=1,而线程2和线程1正好在执行tickets--,结果抢先在线程3之前执行了,tickets=-1了,然后就跳过了判断==0阶段,无限循环停不下来了。

那么问题来了,我判断条件改为tickets<=0不就好了?

    不可以,哪怕这么改动(为了放大现象,加上了Thread.sleep(10)睡眠10ms),比如线程3, 2, 1都在睡眠,此时tickets=1,然后线程3睡醒了,先执行tickets--,打印这是第1号票,接着下一轮循环跳出,线程3结束,然后线程2和线程1睡醒了,依然执行tickets--,依次打印这是第0号票,这是第-1号票。。???卖出负数票了??

那么问题来了,我加上synchronized (this){...}不就好了?

    不可以,这里是new Thread了4次,是4个线程对象,每个对象都有自己的this,互不干扰,这种方法等于没加synchronized。

那么问题来了,我加上锁对象就好了,private Object obj = new Object();再synchronized (obj) {...}

    这个可以,不过这个是成员变量,每个线程对象都有自己的成员变量obj,所以要改为共享的,加上static。

    不过零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码,生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。这里改为private static byte[] lock = new byte[0];再synchronized (lock) {...}会比较好。当然,锁字节码对象也可以。synchronized(TicketSeller.class){...}// 类名.class

那么看看Demo3,用Runnable实现

public class Demo3_Ticket {
    /**
     * @param args
     * 多次开启一条线程是非法的
     */
    public static void main(String[] args) {
        Ticket t = new Ticket();
        new Thread(t, "窗口1").start();
        new Thread(t, "窗口2").start();
        new Thread(t, "窗口3").start();
        new Thread(t, "窗口4").start();
    }
}

class Ticket implements Runnable {
    private int tickets = 100;
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while (true) {
            synchronized (this) {   // ===========提问点提问点提问点=========
                if (tickets == 0) {
                    break;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票");
            }
        }
    }
}

看到提问点

    在这里用synchronized (this) {...}对吗?

        是对的,这里4个线程都是用的同一个Ticket对象。里面的tickets不需要加static,因为这个代码块同时只能一个线程执行,不会有并发问题。也可以synchronized (Ticket.class) {...}。

    这里的synchronized (this) {...}能和上面一句while (true) {...}交换吗?

        可以,但是!就只有窗口1在售票(即线程1执行),执行完才轮到窗口2,然后发现tickets已经为0,线程2结束,同理,线程3,4结束。程序结束。

 

我们要避免死锁问题,我们简化一下哲学家的例子,一个人吃饭,习惯先拿左筷子,另一个人习惯先拿右筷子,每个人拿起一只筷子就不会放下,除非吃完一顿后才放下一双筷子供其他人使用。

见下面一个例子Demo5:

public class Demo4_DeadLock {
    /**
     * 尽量避免同步代码块的嵌套
     */
    private static String s1 = "筷子左";
    private static String s2 = "筷子右";

    public static void main(String[] args) {
        new Thread() {
            public void run() {
                while (true) {
                    synchronized (s1) {
                        System.out.println(getName() + "获取"+ s1 + "等待" + s2);
                        synchronized (s2) {
                            System.out.println(getName() + "获取"+ s2 + "开始吃饭");
                        }
                    }
                    System.out.println("吃完了,放下一双筷子");
                }
            }
        }.start();
        
        new Thread() {
            public void run() {
                while (true) {
                    synchronized (s2) {
                        System.out.println(getName() + "获取"+ s2 + "等待" + s1);
                        synchronized (s1) {
                            System.out.println(getName() + "获取"+ s1 + "开始吃饭");
                        }
                    }
                    System.out.println("吃完了,放下一双筷子");
                }
            }
        }.start();
    }
}

运行结果:

结果互相等待,拿着左筷子的人等待右筷子,拿着右筷子的人等待左筷子。谁也不让谁,就造成了死锁。所以我们要避免synchronized的嵌套使用。

 

更详细的总结见此处:

Java中Synchronized的用法:https://blog.csdn.net/qq_34115899/article/details/80356581

 

========================================Talk is cheap, show me the code=======================================

posted @ 2018-07-15 14:10  绿叶萌飞  阅读(91)  评论(0编辑  收藏  举报