Java的多线程和并发性

2021/7/20 22:08:27

本文主要是介绍Java的多线程和并发性,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

多线程:

并发性:

并发模型与分布式系统的相似性:

多线程的优点:

多线程的代价:

进程与线程:

并行工作者模型:

如何创建并运行java线程:

创建子类还是实现Runnable接口?:

常见错误:调用run()方法而非start()方法:

线程名:

竞态条件与临界区:

线程安全与共享资源:

线程控制逃逸规则:

Java内存模型内部原理:

硬件内存架构

Java 同步块:(synchronized block)

线程通信:

ThreadLocal类:

死锁:

饥饿和公平:

嵌套管程锁死:

Java中的锁:

Java中的读/写锁:

Java信号量(Semaphore):

阻塞队列:

线程池:



多线程:

多线程是在同一个程序内部并行执行,因此会对相同的内存空间进行并发读写操作

并发性:

计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。

并发模型与分布式系统的相似性:

在并发系统中线程之间可以相互通信。在分布式系统中进程之间也可以相互通信(进程有可能在不同的机器中)。线程和进程之间具有很多相似的特性。这也就是为什么很多并发模型通常类似于各种分布式系统架构。

当然,分布式系统在处理网络失效、远程主机或进程宕掉等方面也面临着额外的挑战。但是运行在巨型服务器上的并发系统也可能遇到类似的问题,比如一块CPU失效、一块网卡失效或一个磁盘损坏等情况。虽然出现失效的概率可能很低,但是在理论上仍然有可能发生。

由于并发模型类似于分布式系统架构,因此它们通常可以互相借鉴思想

多线程的优点:

  • 资源利用率更好
  • 程序设计在某些情况下更简单
  • 程序响应更快

多线程的代价:

  • 设计更复杂:在多线程访问共享数据的时候,这部分代码需要特别的注意。线程之间的交互往往非常复杂。不正确的线程同步产生的错误非常难以被发现,并且重现以修复。
  • 增加资源消耗

进程与线程:

进程: 

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序,数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块包含进程的描述信息和控制信息,是进程存在的唯一标志

进程具有的特征:

动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;

并发性:任何进程都可以同其他进行一起并发执行;

独立性:进程是系统进行资源分配和调度的一个独立单位;

结构性:进程由程序,数据和进程控制块三部分组成

线程:

在早期的操作系统中并没有线程的概念,进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

进程和线程的区别:

1. 线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

2. 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线

3. 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信

号等),某进程内的线程在其他进程不可见;

4. 调度和切换:线程上下文切换比进程上下文切换要快得多

应该多使用线程,因为线程廉价,线程启动比较快,退出比较快,对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权,如果使用多重进程,但是不可预期,且测试困难

并行工作者模型:

委派者(Delegator)将传入的作业分配给不同的工作者。每个工作者完成整个任务。工作者们并行运作在不同的线程上,甚至可能在不同的CPU上。

并行工作者模式的优点是,它很容易理解。你只需添加更多的工作者来提高系统的并行度。

并行工作者模型的缺点是,共享状态可能会很复杂,无状态的工作者,任务顺序是不确定的。

如何创建并运行java线程:

一种是创建Thread子类的一个实例并重写run方法,第二种是创建类的时候实现Runnable接口。

第一种编写线程执行代码的方式是新建一个继承自java.lang.Thread或其子类的实例。 创建线程方式为:Tread thread = new Thread();执行该线程方式为:thread.start();

第二种编写线程执行代码的方式是新建一个实现了java.lang.Runnable接口的类的实例,创建线程方式为:class MyRunnable implements Runnable{...}。执行该线程方式为:Thread thread = new Thread(new MyRunnable());thread.start();

创建子类还是实现Runnable接口?:

就我个人意见,我更倾向于实现Runnable接口这种方法。因为线程池可以有效的管理实现了Runnable接口的线程,如果线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而如果线程是通过实现Thread子类实现的,这将会复杂一些。而且Java只能单继承,但可以实现多个接口。

常见错误:调用run()方法而非start()方法:

run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了,想要让创建的新线程执行run()方法,必须调用新线程的start方法。

线程名:

当创建一个线程的时候,可以给线程起一个名字。它有助于我们区分不同的线程。例如:

MyRunnable runnable = new MyRunnable();

Thread thread = new Thread(runnable, "New Thread");

竞态条件与临界区:

竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。多个线程同时读同一个资源不会产生竞态条件。

临界区:导致竞态条件发生的代码区称作临界区。

线程安全与共享资源:

线程安全:允许被多个线程同时执行的代码称作线程安全的代码,线程安全的代码不包含竞态条件。

如何实现线程安全:我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全(成员变量value是通过构造函数赋值的,并且在类中没有set方法。这意味着一旦实例被创建,value变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。)

局部变量:局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。

局部的对象引用:如果在某个方法中创建的对象不会逃逸出,即该对象不会被其它方法获得,也不会被非局部变量引用到该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。当然,如果实例对象通过某些方法被传给了别的线程,那它就不再是线程安全的了。

对象成员:对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。例如

new Thread(new MyRunnable(sharedInstance)).start();

new Thread(new MyRunnable(sharedInstance)).start(); 

当然,如果这两个线程在不同的NotThreadSafe实例上调用call()方法,就不会导致竞态条件。例如:

new Thread(new MyRunnable(new NotThreadSafe())).start();

new Thread(new MyRunnable(new NotThreadSafe())).start();

线程控制逃逸规则:

1.如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

2.即使对象本身线程安全,但如果该对象中包含其他资源(文件,数据库连接),整个应用也许就不再是线程安全的了。

3.如果两个线程同时执行,而且碰巧检查的是同一个记录,那么两个线程最终可能都插入了记录

Java内存模型内部原理:

Java内存模型把Java虚拟机内部划分为线程栈和堆。

每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。

所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。

堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。

一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。

一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。

一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。

静态成员变量跟随着类定义一起也存放在堆上。

存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。

硬件内存架构

一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问寄存器的速度>CPU缓存层>主存。

通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不接见的。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。

Java 同步块:(synchronized block)

有四种不同的同步块:

  1. 实例方法
  2. 静态方法
  3. 实例方法中的同步块
  4. 静态方法中的同步块

实例方法同步:

public synchronized void add(int value){......}

静态方法同步:

public static synchronized void add(int value){....}

实例方法中的同步块:

public void add(int value){synchronized(this){.....}}

静态方法中的同步块:

public static void log2(String msg1, String msg2){synchronized(MyClass.class){...}}

 两个线程引用同一个实例。方法是同步在实例上,是因为方法是实例方法并且被标记synchronized关键字。因此每次只允许一个线程调用该方法。另外一个线程必须要等到第一个线程退出add()方法时,才能继续执行方法。如果两个线程引用了两个不同的实例,那么他们可以同时调用相同的方法。这些方法调用了不同的对象,因此这些方法也就同步在不同的对象上。这些方法调用将不会被阻塞。

线程通信:

线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。

wait():一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。为了调用wait()或者notify(),线程必须先获得那个对象的锁。执行wait方法会释放当前得到的锁

notify():当一个线程调用一个对象的notify()方法,正在等待该对象的所有线程中将有一个线程被唤醒并允许执行,这个将被唤醒的线程是随机的,不可以指定唤醒哪个线程。

notifyAll():唤醒正在等待一个给定对象的所有线程。

一旦一个线程被唤醒,不能立刻就退出wait()的方法调用,直到调用notify()的线程退出了它自己的同步块。换句话说:被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。

假唤醒:由于莫名其妙的原因,线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。

为了防止假唤醒,保存信号的成员变量(boolean wasSignalled = false)将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁(这种做法要慎重,目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大)。被唤醒的线程会自旋直到自旋锁(while循环)里的条件变为false。

在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

ThreadLocal类:

让你创建的变量只被同一个线程进行读和写操作。因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量域。

创建一个ThreadLocal变量:private ThreadLocal myThreadLocal = new ThreadLocal();

存储此对象的值:myThreadLocal.set("A thread local value");

读取一个ThreadLocal对象的值:String threadLocalValue = (String) myThreadLocal.get();

为了使get()方法返回值不用做强制类型转换,通常可以创建一个泛型化的ThreadLocal对象。

private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

初始化ThreadLocal:我们可以通过ThreadLocal子类的实现,并覆写initialValue()方法,就可以为ThreadLocal对象指定一个初始化值。如下所示:

InheritableThreadLocal类:InheritableThreadLocal类是ThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有值,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值。

死锁:

两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候

避免死锁:

  1. 加锁顺序
  2. 加锁时限
  3. 死锁检测

加锁顺序: 例如,线程1拥有锁A和锁B,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

加锁时限:尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行

死锁检测:例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁。当检测出死锁时,一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

饥饿和公平:

1. Java中导致饥饿的原因:

  • 高优先级线程吞噬所有的低优先级线程的CPU时间。
  • 线程被永久堵塞在一个等待进入同步块的状态。
  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)。

2. 在Java中实现公平性方案,需要:

  • 使用锁,而不是同步块。
  • 公平锁。

 通过同步结构在线程间实现公平性:

使用关键字synchronized如果有一个以上的线程调用同一个方法,在第一个获得访问的线程未完成前,其他线程将一直处于阻塞状态,而且在这种多线程被阻塞的场景下,接下来将是哪个线程获得访问是没有保障的。 

使用锁方式替代同步块:不再声明为synchronized,而是用lock.lock()和lock.unlock()来替代。如果存在多线程并发访问lock(),这些线程将阻塞在对lock()方法的访问上。另外,如果锁已经锁上(校对注:这里指的是isLocked等于true时),这些线程将阻塞在while(isLocked)循环的wait()调用里面。要记住的是,当线程正在等待进入lock() 时,可以调用wait()释放其锁实例对应的同步锁,使得其他多个线程可以进入lock()方法,并调用wait()方法。

嵌套管程锁死:

嵌套管程锁死与死锁很像:都是线程最后被一直阻塞着互相等待。

但是两者又不完全相同。在死锁中我们已经对死锁有了个大概的解释,死锁通常是因为两个线程获取锁的顺序不一致造成的,线程1锁住A,等待获取B,线程2已经获取了B,再等待获取A。如死锁避免中所说的,死锁可以通过总是以相同的顺序获取锁来避免。
但是发生嵌套管程锁死时锁获取的顺序是一致的。线程1获得A和B,然后释放B,等待线程2的信号。线程2需要同时获得A和B,才能向线程1发送信号。所以,一个线程在等待唤醒,另一个线程在等待想要的锁被释放。

不同点归纳如下:

1.死锁中,二个线程都在等待对方释放锁。

2.嵌套管程锁死中,线程1持有锁A,同时等待从线程2发来的信号,线程2需要锁A来发信号给线程1

Java中的锁:

Java 5之前,锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字。

创建锁:Lock lock = new Lock();

使用锁:lock.lock();

关闭锁:lock.unlock();

一个Lock类的简单实现:

 锁的可重入性:一个线程在持有一个锁的时候,它内部能否再次(多次)申请该锁。如果一个线程已经获得了锁,其内部还可以多次申请该锁成功。那么我们就称该锁为可重入锁。而不同线程依然不可多次获得锁

锁的公平性:Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用synchronized同步块实现的,因此它们也不保证公平性

在finally语句中调用unlock():这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。

Java中的读/写锁:

读-读能共存,读-写不能共存,写-写不能共存,所以需要一个读/写锁来解决这个问题。

读锁的实现在lockRead()中,只要没有线程拥有写锁(writers==0),且没有线程在请求写锁(writeRequests ==0),所有想获得读锁的线程都能成功获取。

写锁的实现在lockWrite()中,当一个线程想获得写锁的时候,首先会把写锁请求数加1(writeRequests++),然后再去判断是否能够真能获得写锁,当没有线程持有读锁(readers==0 ),且没有线程持有写锁(writers==0)时就能获得写锁。有多少线程在请求写锁并无关系。

ReadWriteLock类中,读锁和写锁各有一个获取锁和释放锁的方法。两个释放锁的方法(unlockRead,unlockWrite)中,都调用了notifyAll方法,而不是notify,假设有线程在等待获取读锁,同时又有线程在等待获取写锁。如果这时其中一个等待读锁的线程被notify方法唤醒,但因为此时仍有请求写锁的线程存在(writeRequests>0),所以被唤醒的线程会再次进入阻塞状态。然而,等待写锁的线程一个也没被唤醒,就像什么也没发生过一样。如果用的是notifyAll方法,所有的线程都会被唤醒,然后判断能否获得其请求的锁。用notifyAll还有一个好处。如果有多个读线程在等待读锁且没有线程在等待写锁时,调用unlockWrite()后,所有等待读锁的线程都能立马成功获取读锁 —— 而不是一次只允许一个。

读锁的重入:

为了让ReadWriteLock的读锁可重入,我们要先为读锁重入建立规则:

  • 满足获取读锁的条件,没有写或写请求
  • 持有读锁,不管是否有写请求

要确定一个线程是否已经持有读锁,可以用一个map来存储已经持有读锁的线程以及对应线程获取读锁的次数,当需要判断某个线程能否获得读锁时,就利用map中存储的数据进行判断。

注意:重入的读锁比写锁优先级高。

写锁的重入:

仅当一个线程已经持有写锁,才允许写锁重入

读锁升级到写锁:

要想让一个拥有读锁的线程,也能获得写锁。就要满足这个线程是唯一一个拥有读锁的线程的要求

写锁降级到读锁:

拥有写锁的线程也能得到读锁。如果一个线程拥有了写锁,那么其它线程是不可能拥有读锁或写锁。

重入锁死:再次请求已经持有的锁会造成重入死锁

Java信号量(Semaphore):

Java 并发库 的Semaphore 可以很轻松完成信号量控制,Semaphore可以控制某个资源可被同时访问的个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

Semaphore实现的功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中 的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了。另外等待的5个人中可以是随机获得优先机会,也可以是按照先来后到的顺序获得机会,这取决于构造Semaphore对象时传入的参数选项。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

Semaphore维护了当前访问的个数,提供同步机制,控制同时访问的个数。在数据结构中链表可以保存“无限”的节点,用Semaphore可以实现有限大小的链表。另外重入锁 ReentrantLock 也可以实现该功能,但实现上要复杂些。 

 规定最多多少个线程同时访问:Semaphore semp = new Semaphore(n);

获取许可:semp.acquire();

访问后释放 :semp.release();

​​​​​​​阻塞队列:

试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,

import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {
	private List queue = new LinkedList();
	private int  limit = 10;
	public BlockingQueue(int limit){
		this.limit = limit;
	}
	public synchronized void enqueue(Object item)
		throws InterruptedException  {
		while(this.queue.size() == this.limit) {
			wait();
		}
		if(this.queue.size() == 0) {
			notifyAll();
		}

		this.queue.add(item);
	}

	public synchronized Object dequeue()
			throws InterruptedException{
		while(this.queue.size() == 0){
			wait();
		}
		if(this.queue.size() == this.limit){
			notifyAll();
		}
		return this.queue.remove(0);
	}
}

必须注意到,在enqueue和dequeue方法内部,只有队列的大小等于上限(limit)或者下限(0)时,才调用notifyAll方法。如果队列的大小既不等于上限,也不等于下限,任何线程调用enqueue或者dequeue方法时,都不会阻塞,都能够正常的往队列中添加或者移除元素。 

线程池:

 线程池(Thread Pool)对于限制应用程序中同一时刻运行的线程数很有用。因为每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等等。

我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。在线程池的内部,任务被插入一个阻塞队列,线程池里的线程会去取这个队列里的任务。当一个新任务插入队列时,一个空闲线程就会成功的从队列中取出任务并且执行它。

线程的好处:

1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

2.提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。

3.提高线程的可管理性

创建一个线程池:

new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, milliseconds,runnableTaskQueue, threadFactory,handler);

corePoolSize(线程池的基本大小),maximumPoolSize(线程池最大大小),keepAliveTime(线程活动保持时间),runnableTaskQueue(任务队列)。

向线程池提交任务:

threadsPool.execute(new Runnable() {
@Override

public void run() {

// TODO Auto-generated method stub

}

});

我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

try {
Object s = future.get();
} catch (InterruptedException e) {
// 处理中断异常
} catch (ExecutionException e) {
// 处理无法执行任务异常
} finally {
// 关闭线程池
executor.shutdown();
}

 线程池的关闭:​​​​​​​

通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow

合理的配置线程池:

  1. 任务的性质:任务性质不同的任务可以用不同规模的线程池分开处理
  2. 任务的优先级:优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
  3. 任务的执行时间:执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。



这篇关于Java的多线程和并发性的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程