读懂Java多线程与并发-基础篇

1.几个重要概念

同步与异步
同步调用会等待方法的返回,异步调用会瞬间返回,但是异步调用瞬间返回并不代表你的任务就完成了,它会在后台起个线程继续进行任务。
阻塞和非阻塞
阻塞和非阻塞通常形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其它所有需要这个资源的线程就必须在这个临界区中进行等待,等待会导致线程挂起。这种情况就是阻塞。此时,如果占用资源的线程一直不愿意释放资源,那么其它所有阻塞在这个临界区上的线程都不能工作。非阻塞允许多个线程同时进入临界区。
并发和并行
并行则是两个任务同时进行,而并发呢,则是一会做一个任务一会又切换到另一个任务。所以单个cpu是不能做到并行的,只能是并发。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用,但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
死锁
是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法进行下去。比如线程1占了B资源,请求A资源,线程2占了A资源,请求B资源。
活锁
线程1可以使用资源,但它让其他线程先使用资源;线程2也可以使用资源,但它也让其他线程先使用资源,于是两者一直谦让,都无法使用资源。

2.使用线程

线程状态转换

有三种方法可以创建线程

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。
public class TestRunnable implements Runnable {
    public void run() {
        
    }
}
public class TestCallable implements Callable<String> {
    public String call() {
        return "HI";
    }
}
public class TestThread extends Thread {
    public void run() {
        
    }
}

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

public static void main(String[] args) {
    TestRunnable test1 = new TestRunnable ();
    Thread t1= new Thread(test1);
    t1.start();
    
    TestCallable test2 = new TestCallable();
    FutureTask<String> ft = new FutureTask<>(test2);
    Thread t2= new Thread(ft);
    t2.start();
    System.out.println(ft.get());

    TestThread t3 = new TestThread();
    t3.start();
}

线程中断
当一个线程正在运行时,另一个线程调用对应的 Thread 对象的 interrupt()方法来中断它,只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。但实际上线程并没有被中断,还会继续往下执行。

线程中断的3个方法

  • interrupt() 中断线程
  • isInterrupted() 判断是否被中断
  • interrupted() 判断是否被中断,并清除当前中断状态
public class TestInterrupt  implements Runnable{  
    public void run(){  
        try{  
            System.out.println("in TestInterrupt() - start");  
            Thread.sleep(20000);  
            System.out.println("in TestInterrupt() - woke up");  
        }catch(InterruptedException e){  
            System.out.println("in TestInterrupt() - interrupted");  
        }  
        System.out.println("in TestInterrupt() - leaving");  
    }  

    public static void main(String[] args) {  
        TestInterrupt  test= new TestInterrupt();  
        Thread t = new Thread(test);  
        t.start();  
        //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("in main() - interrupting other thread");  
        //中断线程t  
        t.interrupt();  
        System.out.println("in main() - leaving");  
    }  
} 

最后会输出 in TestInterrupt () - leaving。

3.线程间协作

线程等待与唤醒
调用 wait() 使得线程被挂起,当其他线程调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于Object,而不属于 Thread。

public class TestWait {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }
    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    TestWait test = new TestWait ();
     new Thread(()->test.after()).start();
     new Thread(()->test.before()).start();
}

wait() 和 sleep() 的区别

  • wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
  • wait() 会释放锁,sleep() 不会。

java.util.concurrent 类库中提供了 Condition 类,可调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。用法跟上面的类似,只是把同步块synchronized换成了同步锁ReentrantLock。
两者的区别:

  • synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
  • synchronized 是非公平的,ReentrantLock 默认情况下也是非公平的,但是可以实现公平锁。
  • ReentrantLock 等待可中断,而 synchronized 不行。
  • ReentrantLock 可以同时绑定多个条件Condition。
  • ReentrantLock 可重入,一个线程可以反复得到相同的一把锁。
public class TestAwait{

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。

public static class TestJoin {
    int num=0;  
    public void  printNum(){
        for (int i = 0; i < 100; i++){
            num=i;
        }
    }
    
    public static void main(String[] args) {
        TestJoin test = new TestJoin ();
        Thread t=new Thread(()->test.printNum()).start();
                 t.join();
                 System.out.println(test.num);
    }
}

4.内存模型

主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量主要是指共享变量。Java 内存模型规定所有的变量都存储在主内存中,而每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值得传递均需要通过主内存来完成。


Java 内存模型中定义了以下 8 种操作来完成主内存与工作内存之间交互的实现细节。

  • luck(锁定):作用于主内存的变量,它把一个变量标示为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到工作内存中,以便随后的 load 动作使用。
  • load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量的值传递到主内存中,以便随后的 write 操作使用。
  • write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量值放入主内存的变量中。

三大特性

  • 原子性:指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其它线程干扰。
  • 有序性:在并发时,程序的执行可能就会出现乱序。因为指令有可能被重排。
  • 内存可见性:指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

5.volatile和ThreadLocal

volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
volatile 是一种稍弱的同步机制,在访问 volatile 变量时不会执行加锁操作,也就不会执行线程阻塞,因此 volatilei 变量是一种比 synchronized 关键字更轻量级的同步机制。
volatile 虽然保证了内存可见性和有序性(添加内存屏障的方式来禁止指令重排),但不能保证原子性,所以是线程不安全的。

ThreadLocal是解决线程安全问题一个很好的思路,ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本,由于Key值不可重复,每一个“线程对象”对应线程的“变量副本”,而到达了线程安全。

public class TestThreadLocal{

//通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){

      public Integer initialValue(){
            return 0;
    }

};

public int getNextNum(){

    seqNum.set(seqNum.get()+1);

     return seqNum.get();

}

public static void main(String[] args) {
    
    TestThreadLocal test = new TestThreadLocal();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
    new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
     new Thread(()->{
          for (int i = 0; i < 3; i++) {
                System.out.println("thread["+Thread.currentThread().getName()+
                  "] num["+test.getNextNum()+"]");
                }
      }) .start();
  }
}

ThreadLocal和线程同步机制比较

  • 同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的。
  • ThreadLocal会为每一个线程拷贝一份独立的变量副本,从而隔离了多个线程对数据的访问冲突。
    当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。

参考资料
高并发Java
线程通信

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

推荐阅读更多精彩内容

  • 线程池ThreadPoolExecutor corepoolsize:核心池的大小,默认情况下,在创建了线程池之后...
    irckwk1阅读 708评论 0 0
  • 第6章类文件结构 6.1 概述 6.2 无关性基石 6.3 Class类文件的结构 java虚拟机不和包括java...
    kennethan阅读 903评论 0 2
  • 1. 计算机系统 使用高速缓存来作为内存与处理器之间的缓冲,将运算需要用到的数据复制到缓存中,让计算能快速进行;当...
    AI乔治阅读 534评论 0 12
  • 最近在摸索如何edit出realistic风格的sims图片,结果还是失败了...不知道是技术不够还是没有用HQ ...
    黑盒君阅读 217评论 0 0
  • 色彩搭配原理与技巧一、从头到脚一般不能超过三种颜色 1、色彩的搭配方法 1、上深下浅:端庄、大方、恬静、严肃 2、...
    Michelle_zhuge阅读 501评论 0 1