Java 16:多线程基础[通俗易懂]

Java 16:多线程基础[通俗易懂]1_callable中可以返回带有计算结果的fature对象,fature是什么

Java

多线程和多进程的本质区别在于进程拥有自己的一整套完整的变量,而线程则共享数据。共享数据使得线程之间的通信比进程更加容易、有效,同时线程更加轻量级,创建和撤销的开销要小得多。

用线程执行任务的简单过程:

1:将任务代码放进实现了Runnable接口的类的run方法中,该接口只有一个抽象方法:public abstract void run(); 

2:创建该类的对象

3:用这个实现了Runnable接口的对象创建一个Thread对象

4:启动线程,start()

注意,不要直接调用Thread类或者Runnable对象的Runnable方法,这样只会执行同一个线程中的任务,而不会启动新进程。应该调用Thread.start()方法,这个方法将创建一个新进程并执行run

当run方法执行完成,或者出现了方法中没有捕获的异常,线程将终止,没有可以强制终止线程的方法(早期的stop方法已经被弃用),interrupt方法可以用来请求终止线程。

当对一个线程调用interrupt方法时,线程的“中断状态”将被置位,这个每个线程都有的boolean标志,线程不时地检查这个标志,以判断线程是否被中断。

可以通过调用静态方法Thread.currentThread来获取当前线程,并调用isInterrupted方法判断,但是如果线程被阻塞,就无法检查中断状态

中断只是引起线程的注意,并不是强制让其终止,被中断的线程可以决定如何相应中断,可以不理会,继续执行,一般情况下,线程简单地认为中断是一个终止请求。

如果每次run循环工作以后都调用了sleep方法,则isInterrupted既没有必要也没有用处,如果目前该线程被sleep阻塞,反而会抛出InterruptedException异常

Thread.static boolean interrupted():静态方法,返回判断的同时会将当前线程的中断状态置为false

Thread.boolean isInterrupted():不会改变线程的中断状态

线程的6中状态:

new(新创建):new Thread(r)以后,线程还没有开始运行

runnable(可运行):调用了start方法以后,一个可运行的线程可能正在运行也可能没有运行(Java没有为正在运行的线程设置专门的状态)

blocked(被阻塞):当线程试图获取一个内部的对象锁(不是java.util.concurrent库中的锁),而该锁被其他线程占有,则进入阻塞状态,当所有相关线程释放该锁,并且线程调度器允许本线程持有它,该线程变成非阻塞状态。

waiting(等待):当线程等待另一个线程通知调度器一个”条件“时,进入等待状态,在调用Onject.wait或者Thread.join方法,或是等待Java.util.concurrent库中的lock或condition时,就会出现这种情况。被阻塞和等待状态有很大的不同

timed waiting(计时等待):Thread.sleep  Object.wait  Thread.join  Lock.tryLock  Condition.await的计时版本,设置时间参数进入,这一状态保持到超时期满或者接收到适当的通知,

terminated(被终止):因为run方法自然退出或者因为一个没有捕获的异常终止了run方法而意外死亡。

当线程出于阻塞或等待状态,不运行代码且消耗最小的资源,直到线程调度器重新激活它。

线程优先级:默认情况下,一个线程继承它父线程的优先级,可以用setPriority设置优先级,最高优先级为10,最低为1,NORM_PRIORITY为5,线程优先级高度依赖系统,比如Windows有7个优先级。

守护线程:通过setDaemon设置,用途是为其他进程提供服务,当只剩下守护进程时,虚拟机就退出了。守护进程永远不要去访问固有资源(文件、数据库),因为它会在任何时候甚至一个操作的中间发生中断。

同步:

race condition(竞争条件):多个线程共享统一数据的存取,当他们都调用修改对象状态的方法,根据调用次序,可能产生错误的情形。

有两种方式防止代码块受并发访问的干扰:synchronized关键字和ReentrantLock类,

ReentrantLock:

locker.lock();//每次是同一个锁,声明成私有域
try{
	//临界区
}finally{
	locker.unlock();//如果临界区代码异常,放在finally中可以保证锁被释放,不阻塞其他线程
}

任何时候只能有一个线程执行由锁保护的代码块。

如果使用锁就不能使用带资源的try语句,因为解锁函数不是close,try首部希望的是声明新变量而锁一般多个线程共用一个即可。

锁是可重入的,线程可以重复获取已经持有的锁,所保持一个hold count来跟踪lock方法的嵌套调用,线程的每一次lock都要调用unlock来解锁(如果lock了多次而没有对应次数的unlock,则控制权一直由该线程占据,其他线程无法进入)

条件对象:

线程进入临界区却发现某一个条件满足以后它才能执行,要使用一个条件对象来管理这些已经获得一个锁但是却不能工作的线程(由于历史原因,该条件对象经常被称为条件变量)。比如进入银行转账的代码中,但账户中钱不够,需要获得别人的转账,但此时又占据锁,别人无法进入临界区。

一个锁对象可以有多个相关的条件对象,可以用newCondition方法获得一个锁对象,每个条件对象管理那些已经进入临界快但是还不能运行的代码。如果发现不满足条件,该Condition调用await()被阻塞,并放弃锁,这是另一个线程就可以进行操作,并可能满足条件。

等待获得锁的线程和调用await的线程存在本质的不同,一旦线程调用await方法,它进入该条件的等待集,当锁可用时,该线程不能马上解除阻塞。直到另一个线程调用同一个条件上的signalAll方法为止。

当另一个线程转账时,他应该调用conditionObject.signalAll()(进入await的线程无法激活自己,一直没有人激活自己,则发生死锁),这一调用激活因为这一条件而等待的所有线程,这些线程从等待集中移出,再次成为可运行的,一旦锁可用,它们中的某一个将从await调用返回,获得锁并从阻塞的地方继续执行,此时线程要再次测试该条件(signalAll只是通知,不能保证条件被满足,另一个signal方法是随机解除等待集中的)

accounts[from]-=amount;		
accounts[to]+=amount;//不加判断的临界区代码,在amount不作判断的基础上,会出现账户的负值
while(accounts[from]<amount){
 sufficientFunds.await();
}
accounts[from]-=amount;		
accounts[to]+=amount;
sufficientFunds.signalAll();

这种情况下保证不会出现账户有负值的情况,但效率肯定也变慢了

synchronized:

大多数情况,并不需要Lock和Condition,java的每一个对象内置了一个锁,如果一个方法用synchronized关键词声明,那么对象的锁将保护整个方法,要想执行该方法,就需要获取对象的内部锁。

public synchronized void method{
 //临界区
}
public void method(){
	this.instrinsicLock.lock();
	try{
		//临界区
	}finally{
		this.instrinsicLock.unlock();
	}
}

两者等价,但是instrinsicLock只是为了方便描述,并不能直接调用

内部锁对象只有一个相关条件,Object的wait方法添加一个线程到等待集中,同理,notify、notifyAll方法解除等待线程的阻塞状态

使用synchronized关键词的版本,代码要简洁很多:

while(accounts[from]<amount){
	wait();
}
accounts[from]-=amount;	
accounts[to]+=amount;		
notifyAll();

静态对象也可以用synchronized修饰,该方法获得类对象的内部锁(Class对象)

内部锁的限制:

不能中断一个视图获取锁的线程

试图获取锁不能超过设定时间

每个内部锁仅有一个条件,可能不够用

如果synchronized满足程序要求,那就尽量不用lock

最好既不用lock/condition也不用synchronized,在许多情况下可以使用java.util.concurrent的一些机制,会为程序处理所有的加锁,比如阻塞队列。

同步阻塞:

前面说到,可以通过调用同步方法获取对象锁,还有一种机制也可以,通过进入一个同步阻塞。
synchronized(obj){  //获取了obj的锁

//临界区

}

synchronized(obj){  //专门用来当做锁的Object域
	accounts[from]-=amount;		
	accounts[to]+=amount;
			
}

这种情况下,如果使用wait和notify会报错,要使用条件,得对obj加锁

while(accounts[from]<amount){
	obj.wait();
}
obj.notifyAll();

监视器:

严格来说,锁和条件不满足面向对象,监视器可以是程序员在不考虑加锁的情况下保证多线程,但是监视器:

是只包含私有域的类

每个监视器类对象有一个锁,该锁对所有方法加锁

Volatile域:

有时候仅仅为了读写一两个实例域就使用同步,开效过大了。

多处理器的计算机能够暂时在寄存器或者本地内存缓存区中保存内存的值,导致运行在不同处理器上的线程可能在统一内存上取到不同的值。同时编译器可以改变指令的顺序,使得吞吐量最大化,虽然不改变代码语义,但是编译器假定内存的值仅仅在代码中显式地修改指令时才会改变,然后多线程环境下,语义顺序遭到外部线程破坏。

如果使用锁来保护被多个线程访问的代码可以不考虑这些问题。

volatile关键字为实例域的访问提供了一种免锁机制,如果域声明为volatile,那么编译器和虚拟机知道该域可能被另一个线程并发更新。

volatile不能保证原子性

volatile的功能有限(在多线程环境下读取一个域),比如在我们的银行程序里,单纯地将账户数组改为volatile并不能确保并发正确。(但是出错的概率远小于什么都不用的情况)

final域:

final域可以保证在构造函数以后才会被线程看到,不会出现null的情况,一个域只能声明成volatile、final中的一个(同样的,final也不能保证线程安全,还是需要同步)

ThreadLocal线程局部变量:比如Rndom类虽然是线程安全的,但是为同时为所有线程提供服务,效率很低,可以为每个线程返回一个单独的对象

读写锁:ReentrantReadWriteLock,可以分别施加读锁和写锁

阻塞队列:

实际编程中应尽量远离地层结构,使用由专业人士开发的较高的层次结构。

对于许多多线程问题,可以通过一个或多个队列来形式化。生产者向队列插入元素,消费者取出元素。使用队列可以安全地从一个线程向另一个线程传递元素。

例如银行转账程序中,转账程序将转账指令的对象插入到队列中,而不是直接访问银行对象,另一个线程从队列中取出指令进行转账。只有该线程可以访问银行对象,因此不需要同步。(当然,队列本身还是得考虑锁和条件,但那是设计者考虑的问题)

当试图向队列添加元素而队列已满,或者想从队列中移除元素而队列为空的时候,阻塞队列(blocking queue)导致线程阻塞。在协调多个线程之间合作的时候,阻塞队列很有用。

阻塞队列方法:

1:返回异常的方法:add、element、remove

2:offer(添加一个元素并返回true,队列满返回false),peek(返回头元素,队列空,返回null),poll(移除并返回头元素,队列空返回null)

3:阻塞方法:put(添加元素,队列满则阻塞),take(移除并返回头元素,空则阻塞)

offer、poll还可以带有超时,在时间内看是否成功,不成功返回false或null

LinkedBlockingQueue是java.util.concurrent包提供的,没有容量边界(也可以构造时设置一个)。它是一个双端队列。

PriorityBlockingQueue是带有优先级的队列,而不是先进先出,元素按照优先级顺序移除,也没有容量上界

DelayQueue

总之,阻塞队列帮助我们,使得不需要显式地同步线程。

线程安全的集合:(包括前面的阻塞队列也是属于线程安全的集合,使得并发条件下,数据结构不遭到破坏,产生异常)

java.util.concurrent提供了映射表、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentLinkedQueue,这些集合使用了复杂的算法,通过“允许并发地访问数据结构的不同部分”来使得竞争最小化

CopyOnWriteArrayList、CopyOnWriteArraySet


ArrayList和HashMap作为早期版本中Vector和HashTable的替代类,失去了它们的线程安全性。但任何集合可以通过使用同步包装器变成线程安全的。

List<String> sybchArrayList=Collections.synchronizedList(new ArrayList<String>());

集合使用锁加以保护,要确保不存在指向原始对象的引用,防止通过原始的费同步方法访问数据结构

如果在另一个线程中对集合进行修改,仍然需要使用客户端锁定

synchronized(synchArrayList)
{
   。。。
}

最好使用java.util.concurrent中定义的类而不是同步包装器。

Callable<V>是Runnable的可以返回异步计算结果的版本,而使用Fature接口可以保存异步计算结果

执行器Executor

构建线程需要一定的代价,因为要和操作系统交互,如果程序创建大量生命周期很短的线程,应该使用线程池。线程池包含许多准备运行的空闲线程,将Runnable对象交给线程池,就会有一个线程调用run方法,当run方法退出时,线程不会死亡。

执行器Executor类有许多静态工厂方法来构造线程池

newCachedThreadPool 必要时创建新线程,空闲线程保留60秒
newFixedThreadPool 保持固定数量的线程,空闲线程会一直保留下去
newSingleThreadExecutor 只有一个线程的池,顺序执行每一个提交的任务,类似于swing的事件分配线程
newScheduledThreadPool 用于预定执行而构建的固定线程池,替代java.util.Timer
newSingleThreadScheduledExecutor  

前三个方法,返回实现了ExecutorService接口的ThreadPoolExecutor类的对象。可以用以下方式将Runnable或Callable对象提交给ExecutorService

Fature<?> submit(Runnable task);
Fature<T> submit(Runnable task,T result);
Fature<T> submit(Callable<T> task);

调用submit会得到一个Fature对象来查询任务的状态

线程池调用shutdown,不再接收新任务,队列中任务都完成以后,线程死亡,而调用shutdownNow则会立刻试图中断正在运行的线程

Fork-Join计算框架

一些应用为每个处理器内核分贝使用一个线程来进行密集计算,java SE 7引入fork-join框架,专门用来支持这类工作。比如将图像分解成上下两部分交给不同的处理器,比如对一个大数组的统计,可以将数组一分为二统计,再将结果相加。

一个使用fork-join的例子:

public interface Filter {
	boolean accept(double t);
}	
public class Counter extends RecursiveTask<Integer>{
	public static final int THRESHOLD=100;
	private int[] values;
	private int from;
	private int to;
	private Filter filter;
	
	public Counter(int[] value,int from,int to,Filter filter){
		this.values=value;
		this.from=from;
		this.to=to;
		this.filter=filter;
	}
	
	@Override
	protected Integer compute() {
		// TODO Auto-generated method stub
		if(to-from<THRESHOLD){
			int count =0;
			for(int i=from;i<to;i++){
				if(filter.accept(values[i])){
					count++;
				}
			}
			return count;
		}else{
			int mid=(from+to)/2;
			Counter first=new Counter(values,from,mid,filter);
			Counter second=new Counter(values,mid,to,filter);
			invokeAll(first,second);
			return first.join()+second.join();
		}
	}	
}
public static void main(String[] args) {
	// TODO Auto-generated method stub
	final int SIZE=1000;
	int[] numbers=new int[SIZE];
	for(int i=0;i<SIZE;i++){
		numbers[i]=i+1;
	}
	Counter counter=new Counter(numbers,0,numbers.length,new Filter(){
		@Override
		public boolean accept(double t) {
			// TODO Auto-generated method stub
			return t>0;
		}
	});
		
	ForkJoinPool pool=new ForkJoinPool();
	pool.invoke(counter);
	System.out.println(counter.join());
}

由于设置的size=1000大于threshold100,所以地柜创建创建了多个Counter非别计算一部分。

最后invokeAll方法接收子任务并阻塞,知道所有任务都完成,join方法生成结果,返回总和

今天的文章Java 16:多线程基础[通俗易懂]分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/85522.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注