【Java并发学习】之线程的同步

【Java并发学习】之线程的同步

前言

在前面一个小节中,我们学习了线程的概念以及在Java中创建任务的方式,并且将任务委托给对应的线程进行执行,本小节我们主要来学习线程之间的关系之一的同步,包含临界区、临界资源、线程同步的两种主要方法

线程的关系

从广义上来讲,线程之间有三种关系

  • 没有关系:多个线程之间相互独立,既不竞争资源,也没有任何的合作关系,只是各自完成自己的任务
  • 竞争关系:两个及以上的线程之间存在对某个或者某些资源的竞争
  • 合作关系:两个及以上的线程共同合作,完成某项任务

临界区及临界资源

学习线程之间的同步,必不可少会接触到临界区以及临界资源这两个概念,而线程之间存在竞争关系本质上就是由于临界资源的存在,而解决的方式就是使得多个线程之间能够序列化访问临界资源

  • 临界资源:临界资源指的是程序中会被多个线程共享的某个或者某些资源,可以是软件资源也可以是硬件资源,比如某个变量,某个数组,某个容器,打印机等等
  • 临界区:临界区指的是访问临界资源的代码,同步操作的主要对象

线程的同步

线程同步是一个非常重要的概念,也是在并发编程中比不可少的关键操作,需要进行同步的本质原因在于,资源的有限,由于资源的数量少于线程的数量,于是线程在访问这些资源的时候需要进行同步处理,如果没有进行同步处理,或者同步处理时不恰当,轻则会导致数据出错,重则会出现严重的并发问题

首先我们来看下没有进行同步处理所带来的后果

情景:假设现在一个公园有三个门,我们需要统计某个时刻公园里的人的总数,由于三个门的统计方式一样,所以我们可以直接采用相同的三个线程来进行统计即可


/**
 * 公园类,包含一个计数器,进入以及离开记录的操作
 */
class Park{
    private static int counter = 0;
    public void enter(){
        counter++;
    }
    public void leave(){
        counter--;
    }
    public int getCounter(){
        return counter;
    }
}

/**
 * 公园的进出登记
 */
class DoorWatcher implements Runnable{

    private Park park;

    public DoorWatcher(Park park) {
        this.park = park;
    }

    @Override
    public void run() {
        while (true){
            park.enter(); // 进入公园
            try {
                Thread.sleep(1000);// 模式人留在公园中的操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            park.leave(); // 离开公园
        }
    }
}

从上面的操作可以看出,如果程序正常执行,那么每个时刻公园中的人数应该是总体上保持稳定的,毕竟每个人进入公园之后会离开公园

对应的测试类如下


 public static void main(String[] args) throws InterruptedException {
        Park park = new Park();

        // 模拟公园的门的计数器
        int doorNumber = 3;
        Runnable jobs[] = new Runnable[doorNumber];
        for (int i = 0; i < doorNumber; i++){
            jobs[i] = new DoorWatcher(park);
        }
        // 执行对应的任务
        ExecutorService executor = Executors.newCachedThreadPool();
        for (int i = 0; i < doorNumber; i++) {
            executor.submit(jobs[i]);
        }
        // 定时检查公园中的人数
        while (true){
            System.out.println("current number in the park is " + park.getCounter());
            Thread.sleep(3000); // 每隔三秒检查一次
        }
    }

测试的可能结果


current number in the park is 0
current number in the park is 2
current number in the park is 2
current number in the park is 2
current number in the park is 3
current number in the park is 3
....
current number in the park is 2
current number in the park is 1
current number in the park is 1
....

执行测试代码之后,可能你会发现实际上程序的运行并不是想象中那样,而且不同次的运行可能结果还不一样,出问题的地方在于counter++以及counter-- 这两个操作,这两个操作在Java中并不是原子操作,关于原子操作,我们会在后面进行深入的学习,这两个操作都包含了取出数据,修改数据,写入数据这三个步骤,而如果没有进行同步处理,则在进行其中任何一个步骤的时候,当前线程可能被挂起,其他线程对counter进行修改,从而导致了数据的不一致,类似的情况还有很多,这里就不进行具体的分析。

由于出现问题的部分是对变量counter的操作,也就是说,这里的counter就是我们所说到的临界资源,而对应的enter以及leave方法则是对应的临界区,或者更详细的说counter++,counter--就是我们所指的临界区

解决线程同步问题的方法从广义上来讲只有一个,那就是序列化访问临界资源,也就是说,同一时刻只允许一个线程来对临界资源进行操作,这种方式有效地解决了同步问题,而具体的操作就是对临界区进行加锁处理

加锁的原理可以简单的理解为,某个线程要进入临界区之间,先申请对应的锁,如果获得该锁,则可以进入,并且将该锁上锁,离开临界区之后就将锁解开;如果没能申请到锁,说明当前时刻临界资源被其他线程占用,则自己进行阻塞,等待锁可以使用

同步方法之使用synchronized

synchronized时Java提供的一个重量级锁,或者称之为监视器,也称之为对象锁,可以用于修饰方法或者代码块,默认锁定的对象是this,也就是当前对象,也可以显示指定所要锁定的对象

修饰方法


class Park{
    private static int counter = 0;
    public synchronized void enter(){
        counter++;
        // ...
    }
    public synchronized void leave(){
        counter--;
        // ...
    }
    // ...
}

修饰代码块


class Park{
    private static int counter = 0;
    public void enter(){
        synchronized(this){
            counter++;
        }
        // ... 
    }
    public void leave(){
        synchronized(this){
            counter--;
        }
        // ...
    }
    // ...
}

synchronizd的使用比较简单,只需要在需要进行同步的方法或者代码块加上该关键字即可,当然,synchronized还有一些比较复杂的原理,这个我们将在后面学习到

同步方法之使用locks

synchronized是在比较旧的JDK中所提供的用于同步的工具,在JDK5之后,还提供了另外的工具用于进行同步,即JUC中的Lock


import java.util.concurrent.locks.ReentrantLock;

class Park{
    private static int counter = 0;

    // 申请一个锁
    private static Lock lock = new ReentrantLock();

    public  void enter(){
        lock.lock();// 加锁
        try {
            counter++;
        }finally {
            lock.unlock();//解锁
        }
    }
    public  void leave(){
        lock.lock();// 加锁
        try {
            counter--;
        }finally {
            lock.unlock();//解锁
        }
    }
    public int getCounter(){
        return counter;
    }
}

从上面的代码中可以看到,使用Lock的操作比较繁琐,我们需要自己申请锁,并且在需要加锁的时候手动加锁,然后在离开的时候进行解锁,可能你会注意到使用时的try...finally代码块,强烈建立在使用Lock的时候采用这种方式,因为在进行资源操作的时候,可能会发生异常,采用这种方式可以保证无论在什么时候都能将锁进行解锁,还记得finally的作用吗?_

Lock的使用虽然比较繁琐,而且还需要自己手动加锁、解锁,但是Lock也有synchronized所不具备的特点,那就是灵活,关于这两者的具体区别,我们将在后面的内容中学习到

总结

本小节我们主要学习了线程同步的概念,临界资源、临界区的概念,没有加锁的可能带来的危害,以及常见的同步方式,synchronized的使用以及Lock使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容