多线程

1. 什么是线程

线程(thread)是指一个任务从头至尾的执行流,线程提供一个运行任务的机制,对于java而言,一个程序中可以并发的执行多个线程,这些线程可以在多处理器系统上同时运行。当程序作为一个应用程序运行时,java解释器为main()方法启动一个线程。

2. 线程和进程

区别和联系:
1. 进程中可以包含多个线程,线程必须存在于某个进程实体中

2. 进程在进行上下文切换时由于要切换页表,往往伴随者页调
   度,因此开销比较大,而线程在进行上下文切换时,由于仅
   涉及与自身相关的寄存器状态和栈的信息(线程的上下文环
   境主要包含寄存器的值、程序计数器、栈指针),因此开销
   比较小;

3. 系统在运行时会为每个进程分配不同的内存区域,但是不会
   为线程分配内存,同一个进程中的各个线程共享该进程的内
   存区域。

4. 与进程的控制表PCB相似,线程也有自己的控制表TCB,但是
   TCB中所保存的线程状态比PCB表中少多了。

5. 独立进程间的通信要与核心交互,而由于同一进程中的线程
   共享内存,它们之间的通信就不需要调用核心。

3. 线程创建

  • 需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法;
  • 实现Runnalbe接口,重载Runnalbe接口中的run()方法。
两者区别:

使用实现Runnable接口方式创建线程可以共享同一个目标对象(TreadDemo1 tt=new TreadDemo1();),实现了多个相同线程处理同一份资源.而继承Thread创建线程的方式,new出了两个任务类对象,有各自的成员变量,相互之间不干扰。

一、采用继承Thread类方式:

(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。

二、采用实现Runnable接口方式:

(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。

4. 线程的生命周期

Paste_Image.png

(1)生命周期的五种状态

新建(new Thread)
当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。
  例如:Thread t1=new Thread();

就绪(runnable)
线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。例如:t1.start();

运行(running)
线程获得CPU资源正在执行任务(run()方法),此时除非此线程自动放弃CPU资源或者有优先级更高的线程进入,线程将一直运行到结束。

死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。
  自然终止:正常运行run()方法后终止
  异常终止:调用stop()方法让一个线程终止运行

堵塞(blocked)
由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。
  正在睡眠:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。
  正在等待:调用wait()方法。(调用notify()方法回到就绪状态)
  被另一个线程所阻塞:调用suspend()方法。(调用resume()方法恢复)

5. 线程安全

1). 什么是线程安全?线程安全是怎么完成的(原理)?

 线程安全就是说多线程访问同一代码,不会产生不确定的结果。编写线程安全的代码是低依靠线程同步。

2). 线程安全的类:

StringBuffer:线程安全的可变字符序列。一个类似于 String的字符串缓冲区,但不能修改。
Vector:Vector类可以实现可增长的对象数组。
Hashtable:此类实现一个哈希表,该哈希表将键映射到相应的值。任何非 null对象都可以用作键或值。

API 搜索Collections,查看方法

A:public static <T> Collection<T> synchronizedCollection(Collection<T> c):返回指定 collection 支持的同步(线程安全的)collection。

B:public static <T> List<T> synchronizedList(List<T> list):返回指定列表支持的同步(线程安全的)列表。

C:public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m):返回由指定映射支持的同步(线程安全的)映射。

D:public static <T> Set<T> synchronizedSet(Set<T> s):返回指定 set 支持的同步(线程安全的)set。

E:public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m):返回指定有序映射支持的同步(线程安全的)有序映射。

F:public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s):返回指定有序 set 支持的同步(线程安全的)有序 set。

6.线程锁

1. synchronized

把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)可见性(visibility)
1 原子性
  原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。

2 可见性
  可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为。它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。

作用:如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

原理:当对象获取锁时,它首先使自己的高速缓存无效,这样就可以保证直接从主内存中装入变量。 同样,在对象释放锁之前,它会刷新其高速缓存,强制使已做的任何更改都出现在主内存中。 这样,会保证在同一个锁上同步的两个线程看到在 synchronized 块内修改的变量的相同值。

一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。

——volatile只保证可见性,不保证原子性!

synchronize的限制

synchronized是不错,但它并不完美。它有一些功能性的限制:

  1. 它无法中断一个正在等候获得锁的线程;
  2. 也无法通过投票得到锁,如果不想等下去,也就没法得到锁;
  3. 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。

2.ReentrantLock

Java.util.concurrent.lock 中的Lock框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为Lock的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。
ReentrantLock类实现了Lock,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票定时锁等候可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。

class Outputter1 {    
    private Lock lock = new ReentrantLock();// 锁对象    
  
    public void output(String name) {           
        lock.lock();      // 得到锁    
  
        try {    
            for(int i = 0; i < name.length(); i++) {    
                System.out.print(name.charAt(i));    
            }    
        } finally {    
            lock.unlock();// 释放锁    
        }    
    }    
} 

区别:

需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!

3.读写锁ReadWriteLock

上例中展示的是和synchronized相同的功能,那Lock的优势在哪里?
例如一个类对其内部共享数据data提供了get()和set()方法,如果用synchronized,则代码如下:

class syncData {        
    private int data;// 共享数据        
    public synchronized void set(int data) {    
        System.out.println(Thread.currentThread().getName() + "准备写入数据");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        this.data = data;    
        System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
    }       
    public synchronized  void get() {    
        System.out.println(Thread.currentThread().getName() + "准备读取数据");    
        try {    
            Thread.sleep(20);    
        } catch (InterruptedException e) {    
            e.printStackTrace();    
        }    
        System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
    }    
}    

然后写个测试类来用多个线程分别读写这个共享数据:

public static void main(String[] args) {    
//        final Data data = new Data();    
          final syncData data = new syncData();    
//        final RwLockData data = new RwLockData();    
          
        //写入  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
                @Override  
        public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.set(new Random().nextInt(30));    
                    }    
                }    
            });  
            t.setName("Thread-W" + i);  
            t.start();  
        }    
        //读取  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
                @Override  
        public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.get();    
                    }    
                }    
            });    
            t.setName("Thread-R" + i);  
            t.start();  
        }    
    }    

运行结果:

Thread-W0准备写入数据  
Thread-W0写入0  
Thread-W0准备写入数据  
Thread-W0写入1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R1准备读取数据  
Thread-R1读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R2读取1  
Thread-R0准备读取数据 //R0和R2可以同时读取,不应该互斥!  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-W1准备写入数据  
Thread-W1写入18  
Thread-W1准备写入数据  
Thread-W1写入16  
Thread-W1准备写入数据  
Thread-W1写入19  
Thread-W1准备写入数据  
Thread-W1写入21  
Thread-W1准备写入数据  
Thread-W1写入4  
Thread-W2准备写入数据  
Thread-W2写入10  
Thread-W2准备写入数据  
Thread-W2写入4  
Thread-W2准备写入数据  
Thread-W2写入1  
Thread-W2准备写入数据  
Thread-W2写入14  
Thread-W2准备写入数据  
Thread-W2写入2  
Thread-W0准备写入数据  
Thread-W0写入4  
Thread-W0准备写入数据  
Thread-W0写入20  
Thread-W0准备写入数据  
Thread-W0写入29  

现在一切都看起来很好!各个线程互不干扰!等等。。读取线程和写入线程互不干扰是正常的,但是两个读取线程是否需要互不干扰??
对!读取线程不应该互斥!

我们可以用读写锁ReadWriteLock实现:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class Data {        
    private int data;// 共享数据    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到写锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备写入数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 释放写锁    
        }    
    }       
  
    public void get() {    
        rwl.readLock().lock();// 取到读锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备读取数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 释放读锁    
        }    
    }    
} 

测试结果:

Thread-W1准备写入数据  
Thread-W1写入9  
Thread-W1准备写入数据  
Thread-W1写入24  
Thread-W1准备写入数据  
Thread-W1写入12  
Thread-W0准备写入数据  
Thread-W0写入22  
Thread-W0准备写入数据  
Thread-W0写入15  
Thread-W0准备写入数据  
Thread-W0写入6  
Thread-W0准备写入数据  
Thread-W0写入13  
Thread-W0准备写入数据  
Thread-W0写入0  
Thread-W2准备写入数据  
Thread-W2写入23  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入17  
Thread-W2准备写入数据  
Thread-W2写入11  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取11  
Thread-R1读取11  
Thread-R2读取11  
Thread-W1准备写入数据  
Thread-W1写入18  
Thread-W1准备写入数据  
Thread-W1写入1  
Thread-R0准备读取数据  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0读取1  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R2读取1  
Thread-R0读取1  

与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)
从理论上讲,与互斥锁定相比,使用读-写锁定所允许的并发性增强将带来更大的性能提高。

在实践中,只有在多处理器上并且只在访问模式适用于共享数据时,才能完全实现并发性增强。——例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁定的理想候选者。

4.volatile

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。

5. 读写锁和互斥锁

读写锁特点:
1)多个读者可以同时进行读
2)写者必须互斥(只允许一个写者写,也不能读者写者同时进行)
3)写者优先于读者(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
互斥锁特点:
一次只能一个线程拥有互斥锁,其他线程只有等待

7.

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

推荐阅读更多精彩内容