线程的创建与同步

线程的创建

Java提供两种创建线程的方式:1.继承Thread类并实现run方法,2.实现Runnable接口。
Thread(Runnable target, String name)
静态代理模式
target:被代理对象
thread:代理对象
静态代理模式需要两部:1.代理对象和被代理对象实现了相同的接口2.代理对象被拥有代理对象的引用

public class ThreadDemo {
  public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
    System.out.println(Thread.currentThread().getName() + " is running ...");
  }
}

public class MyThread extends Thread {
  public void run() {
    System.out.println(Thread.currentThread().getName() + " is running ...");
  }
}

这里创建类一个MyThread类,实现run()方法,输出当前线程的名字。main方法中,新建MyThread类的实例后调用了实例的start()方法,该方法会将新的线程状态置为RUNNABLE,新的线程会在被调度到后开始执行。

public class RunnableDemo {
  public static void main(String[] args) {
    Runnable runner = new Runnable() {
      @Override
      public void run() {
        System.out.println(Thread.currentThread().getName() + " is running ...");
      }
    };

    new Thread(runner).start();
    System.out.println(Thread.currentThread().getName() + " is running ...");
  }
}

这里实现Runnable接口后,匿名实例runner只说明了如何在新的线程中运行(即run方法),而没有说明以哪个线程去运行。新建一个Thread类的实例,将runner作为参数传入,并调用线程的start()方法,runner所描述的工作才会在新的线程中执行。

这两种方式如何选择呢?当一个类已经继承了别的类了,要将此类当作一个线程,那么就只能实现Runnable接口;而选择将一个类继承Thread类的时候,这个类的名字最好取为一个能发出动作或者执行操作的名词,比如 Calculator extends Thread 说明Calculator这个类能执行某种操作,比如calculate。

线程的状态

Java 线程一般分为5个状态: NEW,RUNNABLE,RUNNING,BLOCKED/WAITING, DEAD。

  • NEW: 当一个Thread类被创建后还未调用start() 之前,线程处于NEW状态;
  • RUNNABLE: 线程调用start()方法过后,调度器还没有选中线程进行执行之前;
  • RUNNING: 线程调度器选中线程,线程开始执行;
  • BLOCKED/WAITING:线程被阻塞或者被挂起;
  • DEAD: 线程被销毁。


    image.png

线程的sleep()方法和yield()方法有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,running-->blocked. 而执行yield()方法后转入就绪(ready)状态; running-->runnable
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。

线程同步

在堆上分配内存的对象的成员变量可以被多个线程同时访问,当这些线程可以执行写操作时,这样的共享内存位置就变成了临界区资源。访问临界区资源的代码即为临界代码区,临界代码区需要通过同步机制才能保证临界区资源修改的正确性。


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

public class SyncDemo {
  public static void main(String[] args) {
    new SyncDemo().demo();
  }

  public void demo() {
    Bank bank = new Bank();
    List<Saver> savers = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
      Saver saver = new Saver(bank);
      savers.add(saver);
      saver.start();
    }

    List<Spender> spenders = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
      Spender spender = new Spender(bank);
      spenders.add(spender);
      spender.start();
    }

    try {
      for (Saver s : savers) {
        s.join();
      }
      for (Spender s : spenders) {
        s.join();
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    }

    System.out.println(bank.getTotalAmount());
  }

  class Bank {
    private double totalAmount = 0;

    public void withdraw(double amount) {
      double newAmount = totalAmount - amount;
      try {
        Thread.sleep((long) Math.random() * 100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      totalAmount = newAmount;
    }

    public void deposit(double amount) {
      double newAmount = totalAmount + amount;
      try {
        Thread.sleep((long) Math.random() * 100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      totalAmount = newAmount;
    }

    public double getTotalAmount() {
      return totalAmount;
    }
  }

  class Saver extends Thread {
    private Bank bank;

    public Saver(Bank bank) {
      this.bank = bank;
    }

    public void run() {
      bank.deposit(100);
    }
  }

  class Spender extends Thread {
    private Bank bank;

    public Spender(Bank bank) {
      this.bank = bank;
    }

    public void run() {
      bank.withdraw(100);
    }
  }
}

这个例子中,100个存钱线程同时向一个银行存钱,100个取钱线程同时取钱,最终的余额应该为0才对,但是由于临界区资源没有同步访问控制,导致结果并不正确。这个例子中临界代码区为方法withdraw和deposit。当多个线程同时进入临界区时,每个线程执行的速度是不确定的,每个线程也可能在临界区中执行时被操作系统挂起。例如,假如线程A和B同时进入withdraw方法,A、B的执行顺序可能为:

A: double newAmount = totalAmount - amount;
B: double newAmount = totalAmount - amount;
A: totalAmount = newAmount;
B: totalAmount = newAmount;

假设totalAmount一开始为0,那么第一行执行后newAmount=-100, totalAmount=0, 第二行执行时totalAmount=0,所以执行后newAmount=-100,第三行和第四行执行后totalAmount=-100。而正确的结果应该是-200。

Java提供synchronized关键字、同步锁等方式实现线程同步。

synchronized关键字

synchronized关键字有两种用法:1.作用于方法上,2.作用于一个对象。Java每个对象都有一个内置锁,当synchronized修饰一个对象作用于一片代码区域时,内置锁会用于控制临界区只允许一个线程单独进入。

    public synchronized void withdraw(double amount) {
      ...
    }

    public void deposit(double amount) {
      synchronized (this) {
        double newAmount = totalAmount + amount;
        try {
          Thread.sleep((long) Math.random() * 100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        totalAmount = newAmount;
      }
    }

同步锁

    private Lock lock = new ReentrantLock();

    public synchronized void withdraw(double amount) {
      lock.lock();
      double newAmount = totalAmount - amount;
      try {
        Thread.sleep((long) Math.random() * 100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      totalAmount = newAmount;
      lock.unlock();
    }

wait/notify

假设在存取钱的例子中我们要求存款余额不能为负数,当余额为0时,取钱者需要等待直到有存钱者存入钱。那么当余额为0 时,withdraw方法需要被挂起并等待存钱线程存钱。

    public synchronized void withdraw(double amount) {
      if (totalAmount <= 0) {
        try {
          wait();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      double newAmount = totalAmount - amount;
      try {
        Thread.sleep((long) Math.random() * 100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      totalAmount = newAmount;
    }

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

推荐阅读更多精彩内容

  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    小徐andorid阅读 2,805评论 3 53
  • 一、进程和线程 进程 进程就是一个执行中的程序实例,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程。...
    阿敏其人阅读 2,612评论 0 13
  • Java多线程学习 [-] 一扩展javalangThread类 二实现javalangRunnable接口 三T...
    影驰阅读 2,955评论 1 18
  • 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 首先讲...
    李欣阳阅读 2,452评论 1 15
  • 有的时候,结果是可以预测的。 趋势总是无法改变的,但我们可能低估了趋势的力量。它就像一股汹涌的浪潮,可以堙没一切。...
    一叶随风天下知秋阅读 506评论 0 0