Java 多线程(三)——线程同步

Java 多线程(三)——线程同步

一、提出问题

通过 2 个线程,来实现 a、b 的随机逐行打印,每行 10 个字母逐字打印。
效果类似如下:

aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb
aaaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb

二、错误案例源码

代码:

  1. 我们先写一个打印实现类 Out

    public class Out {
        public void print(String str) {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        }
    }
  2. 写一个 main 方法,通过 2 个线程来调用 Out 的 print 方法

    public static void main(String[] args) {
    
            final Out out = new Out();
    
            Thread t1 = new Thread(new Runnable(){
                @Override
                public void run() {
                    while (true) {
                        out.print("aaaaaaaaaa");
                    }
                }
            });
            Thread t2 = new Thread(new Runnable(){
                @Override
                public void run() {
                    while (true) {
                        out.print("bbbbbbbbbb");
                    }
                }
            });
            t1.start();
            t2.start();
        }

结果:
代码看起来没问题,但运行起来却发现

bbbbbbbbbb
aaaaaaaaa
aaaaaaaaaa
bbaaabbbbbbaaabb
bbbbabbbbbb

原因:
在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,看谁先得到CPU的资源。
说白了,就是CPU对线程执行的随机调度,比如上面的 t1 线程此时正在打印信息还没打印完毕此时 CPU 切换到 t2 线程执行了,t2 线程执行完了又切换回 t1 线程执行就会导致上面现象发生。

解决办法
解决办法就是把多个线程要调用的同一方法或者操作的同一变量加上一把锁,只有得到了这个锁,才能进行调用。
具体方法有以下几种:

  1. 同步方法
  2. 同步代码块
  3. 静态函数的同步
  4. Lock锁机制

三、同步方法

其实就是个方法加上 synchronized 关键字。

代码:

public class Out {

    public synchronized void print(String str) {
        for (int i = 0; i < str.length(); i++) {
            System.out.print(str.charAt(i));
        }
        System.out.println();
    }
}

结果:

aaaaaaaaaa
aaaaaaaaaa
bbbbbbbbbb
bbbbbbbbbb
bbbbbbbbbb
aaaaaaaaaa

原因:
为 print 方法加上 synchronized 后其就变成了同步方法,普通同步方法的锁是 this,也就是当前对象,比如案例中,外部要想调用 print 方法就必须创建 Out 类实例对象 out,此时 print 同步方法的锁就是这个 out。

四、同步代码块

其实 synchronized 还可以用在代码快上。
代码:

  1. 首先,我们为 Out 类添加 print1 方法

    public void print1(String str) {
        synchronized (str) {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        }
    }
  2. 让 t2 线程调用 print1 方法

    public static void main(String[] args) {
    
        final Out out = new Out();
    
        Thread t1 = new Thread(new Runnable(){
            @Override
            public void run() {
                while (true) {
                    out.print("aaaaaaaaaa");
                }
            }
        });
        Thread t2 = new Thread(new Runnable(){
            @Override
            public void run() {
                while (true) {
                    out.print1("bbbbbbbbbb");
                }
            }
        });
        t1.start();
        t2.start();
    }

结果:
但是一运行,就又出问题了。

bbbbbbbbbb
bbbbbbaaaaaaaa
aaaaaaaaaa
aaaaaaaaaa
bbbb

原因:
同步代码块的写法是 synchronized(obj){},其中 obj 为锁对象。
因为 print 与 print1 方法的锁不一样导致的,线程 t1 调用 print 方法拿到 this 这把锁,线程 t2 调用 print1 拿到 str 这把锁,二者互不影响。

解决:
其实解决办法很简单,修改 print1 方法,使其锁对象为 this。

public void print1(String str) {
        synchronized (this) {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        }
    }

五、静态函数的同步

复制 print 方法,命名为 print2,并为其添加 static 关键字,使其为静态方法。

代码:

  1. print2 方法

    public synchronized static void print2(String str) {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        }
  2. 让 2 个线程分别调用 print1、print2 方法

    public static void main(String[] args) {
    
            final Out out = new Out();
    
            Thread t1 = new Thread(new Runnable(){
                @Override
                public void run() {
                    while (true) {
                        out.print1("aaaaaaaaaa");
                    }
                }
            });
            Thread t2 = new Thread(new Runnable(){
                @Override
                public void run() {
                    while (true) {
                        Out.print2("bbbbbbbbbb");
                    }
                }
            });
            t1.start();
            t2.start();
        }

结果:
但是一运行,就又出问题了。

aaaaaaaaaa
aaaaaabbbbbbbbbb
bbbbbbbbbb
aaaa

原因:
其实这里的原因更上一个很相像,因为 print2 方法是静态方法,静态方法是没有对应类的实例对象的,所以它的锁并不是 this,它的锁是 类的字节码对象。并且类的字节码对象是优先于类实例对象存在的。

解决:
解决办法就是调整 print1 方法的锁对象

public void print1(String str) {
        synchronized (Out.class) {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        }
    }

六、Lock锁机制

从 JDK1.5 起我们就可以根据需要显性的获取锁以及释放锁了,这样也更加符合面向对象原则。
Lock 接口的实现子类之一 ReentrantLock,翻译过来就是重入锁,就是支持重新进入的锁,该锁能够支持一个线程对资源的重复加锁,也就是说在调用 lock() 方法时,已经获取到锁的线程,能够再次调用 lock() 方法获取锁而不被阻塞。
同时还支持获取锁的公平性和非公平性,所谓公平性就是多个线程发起 lock() 请求,先发起的线程优先获取执行权,非公平性就是获取锁与是否优先发起 lock() 操作无关。
默认情况下是不公平的锁。因为保证公平就必须需要额外开销,这样性能就下降了,所以公平与性能是有一定矛盾的,除非公平策略对你的程序很重要,比如必须按照顺序执行线程,否则还是使用不公平锁为好。

代码:
添加一个 print3 方法

    private Lock lock = new ReentrantLock();
//  private Lock lock = new ReentrantLock(true);
    public synchronized void print3(String str) {
        lock.lock(); // 如果有其它线程已经获取锁,那么当前线程在此等待直到其它线程释放锁。
        try {
            for (int i = 0; i < str.length(); i++) {
                System.out.print(str.charAt(i));
            }
            System.out.println();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 释放锁资源
            // 使用 try{}catch{}finally{},是为了保证 锁资源的释放
            // 防止因为代码的异常而 死锁
        }
    }

七、总结

普通同步函数的锁是 this,当前类实例对象,同步代码块锁可以自己定义,静态同步函数的锁是类的字节码文件,不同锁之间互不影响。

synchronized:

  • synchronized 是在 JVM 层面上实现的(所有对象都自动含有单一的锁。
  • synchronized 在锁定时如果方法块抛出异常,JVM 会自动将锁释放掉。
  • 在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized 是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好。

Lock:

  • Lock 的锁定是通过代码实现的。
  • Lock 出现异常时必须在 finally 将锁释放掉,否则将会引起死锁。
  • 在资源竞争激烈情况下,Lock同步机制性能会更好一些。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值