线程同步之synchronized关键字

从Java线程到kotlin协程 专栏收录该内容
15 篇文章 2 订阅

上一篇:

线程合并 (join)

线程的同步

简单来讲,当多个线程要对同一个内存地址进行操作时(一般都是操作),同一时间只能有一个线程对该内存地址进行操作,其他线程不可以对该内存地址进行操作,此时,其他线程处于BLOCKED (阻塞)状态,这个就是线程同步。

线程同步主要是为了确保线程安全

我们先来看个经典的卖票例子。

举个例子,总共有30张票,分5个窗口同时售卖,不加同步的代码如下


object Ticket {
    /*总共票数*/
    var totalCount = 30

    /*当前售出的票数*/
    private var currentNumber = 1

    /*卖票*/
    fun sale() {
        if (totalCount == 0) {
            println("${Thread.currentThread().name} 票卖完了")
            return
        }

        totalCount--
        println("${Thread.currentThread().name} 卖出第${currentNumber}张票,剩余${totalCount}张票")
        currentNumber++

        TimeUnit.MILLISECONDS.sleep(100)
    }
    
}


main方法如下,模拟5个窗口同时卖票

fun main() {
    for (i in 1..5) {
        Thread({
            while (Ticket.totalCount > 0) {
                Ticket.sale()
            }
        }, "窗口" + i).start()

    }
}


运行结果如下:
在这里插入图片描述

可以看到,卖票的数据明显是不对的。

原因是多个线程同时对Ticket中的count做了操作,造成了数据不安全,导致其他线程访问的数据不是正确的数据。

像这种多个线程同时对同一个内存地址做操作,为了保证数据安全,我们就需要做线程同步操作

synchronized 关键字

synchronized是JVM提供的关键字,该关键字相当于给资源上锁,要访问资源就必须先拿到锁,如果锁被其他线程持有,则当前线程处于BLOCKED(阻塞)状态,等待锁释放,然后去争夺锁获得执行机会。

synchronized 可以用在以下代码中

  • 实例方法(非静态方法) 锁的是调用的对象
  • 静态方法 锁的是
  • 代码块

还是上面的代码,用Java版本加上synchronized 关键字来看一下


public class TestSynchronized {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {

                while (Ticket.totalCount > 0) {
                    Ticket.sale();
                }

            }, "窗口" + i).start();
        }
    }


}

class Ticket {
    public static int totalCount = 30;

    private static int number = 1;

    /*卖票*/
    public static synchronized void sale() {
        if (totalCount == 0) {
            System.out.println(Thread.currentThread().getName() + "票卖完了");
            return;
        }
        totalCount--;
        System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
        number++;
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可以看到,我们给sale静态方法加上了synchronized关键字,下面来看看运行结果

在这里插入图片描述
可以看到,这下数据就是正常的了。

需要注意的是,在使用synchronized的时候,要注意synchronized锁的资源是否是同一个,只有锁的资源是同一个,同步代码才会生效,否则,就不是同步执行的了。

上面我们那个是锁的 静态方法,下面我们来试试锁代码块

class Ticket {
    public static int totalCount = 30;

    private static int number = 1;

    /*卖票*/
    public static void sale() {

        synchronized (Ticket.class) {
            if (totalCount == 0) {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                return;
            }
            totalCount--;
            System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
            number++;
        }

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

上面我们同步代码块锁住的是Ticket.class,我们只需要锁住需要被同步的代码即可。
我们再来看看运行结果:
在这里插入图片描述
可以看到,结果是符合预期的。
下面我们来试试锁住不同的资源试一下,代码如下

class Ticket {
    public static int totalCount = 30;

    private static int number = 1;

    /*卖票*/
    public static void sale() {

        synchronized (new Ticket()) {
            if (totalCount == 0) {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                return;
            }
            totalCount--;
            System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
            number++;
        }

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

上面代码我们的同步代码块锁是每次都new一个对象,这样,锁肯定不是同一个。再来看下运行结果:
在这里插入图片描述
运行结果可以看到,数据又变成不正确的了。

锁实例方法用法也是类似,代码如下:

public class TestSynchronized {

    public static void main(String[] args) {


        Ticket ticket = new Ticket();
        
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {

                while (ticket.totalCount > 0) {
                    ticket.sale();
                }

            }, "窗口" + i).start();
        }
    }


}

class Ticket {
    public int totalCount = 30;

    private int number = 1;

    /*卖票*/
    public synchronized void sale() {


        if (totalCount == 0) {
            System.out.println(Thread.currentThread().getName() + "票卖完了");
            return;
        }
        totalCount--;
        System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
        number++;

        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

运行结果如下:
在这里插入图片描述

可以看到,结果是正常的。

下面我们再来看看不同实例的同步方法运行是什么样的。
main方法代码如下:

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {

                Ticket ticket = new Ticket();
                while (ticket.totalCount > 0) {
                    ticket.sale();
                }

            }, "窗口" + i).start();
        }
    }

运行结果如下
在这里插入图片描述

这个就相当于,起了5个线程,每个线程单独执行任务,跟同步没啥关系了。

所以,在使用synchronized关键字的时候,我们要特别注意线程争夺的 是否是同一个。

还是上面的代码,我们只需要保证,不同线程拿到的实例是同一个就行。
这就是日常开发中很常见的单例模式。

下面我们来改造下代码,如下:


class Ticket {


    private static Ticket ticket = null;


    public int totalCount = 30;

    private int number = 1;


    private Ticket() {

    }


    /*线程安全的单例模式*/
    public static Ticket newInstance() {

        if (ticket == null) {
            synchronized (Ticket.class) {

                if (ticket == null) {
                    synchronized (Ticket.class) {
                        ticket = new Ticket();
                    }
                }

            }
        }
        return ticket;
    }

    /*卖票*/
    public void sale() {

        synchronized (Ticket.class) {
            if (totalCount == 0) {
                System.out.println(Thread.currentThread().getName() + "票卖完了");
                return;
            }
            totalCount--;
            System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,剩余" + totalCount + "张票");
            number++;
        }
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
}

main方法

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {

                Ticket ticket = Ticket.newInstance();

                System.out.println("ticket对象地址" + ticket);
                while (ticket.totalCount > 0) {
                    ticket.sale();
                }

            }, "窗口" + i).start();
        }
    }


我们再来看下运行结果:

在这里插入图片描述

可以看到,由于我们将获取Ticket对象写成了线程安全的单例模式,所以,就不会出现5个对象各自执行的情况了。

下面简单用kotlin实现一下同样的效果,代码如下:

fun main() {
    for (i in 1..5) {


        val ticket = Ticket.newInstance

        println("ticket = ${ticket}")
        Thread({
            while (ticket.totalCount > 0) {
                ticket.sale()
            }
        }, "窗口" + i).start()

    }

}

class Ticket private constructor() {


    /*总共票数*/
    var totalCount = 30

    /*当前售出的票数*/
    private var currentNumber = 1

    companion object {
        /*线程安全的懒汉式单例*/
        val newInstance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { Ticket() }
    }

    /*卖票*/
    fun sale() {

        synchronized(Ticket::class.java) {
            if (totalCount == 0) {
                println("${Thread.currentThread().name} 票卖完了")
                return
            }

            totalCount--
            println("${Thread.currentThread().name} 卖出第${currentNumber}张票,剩余${totalCount}张票")
            currentNumber++

        }

        TimeUnit.MILLISECONDS.sleep(100)
    }

}

运行结果如下:
在这里插入图片描述

也是ok的,没有什么问题

好了,synchronized关键字大概就是这些。

下一篇:

线程间的通信 wait、notify


如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!

  • 1
    点赞
  • 3
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值