Java面向对象之多线程运行机制
Java较为实用的多线程设计与应用
多线程机制讨论
电梯调度算法
请求分配顺序
自动化测试方法
- 生成随机数据
- 验证输出数据正确性
- 打包自己的代码文件
- 运行评测机
线程安全与锁:https://www.cnblogs.com/zhaojinhui/p/5526988.html
- lock
- condition
- reentrantlock
- Semaohrone信号量
代码耦合性和内聚性
同一个模块内的各个元素之间要高度紧密,但是各个模块之间的相互依存度却要不那么紧密。 内聚和耦合是密切相关的,同其他模块存在高耦合的模块意味着低内聚,而高内聚的模块意味着该模块同其他模块之间是低耦合。在进行软件设计时,应力争做到高内聚,低耦合。
降低耦合
一个模块访问另一个模块的内容; 模块间传递数据结构为而不是单一数据
增强内聚
逻辑上独立模块; 时间上同时执行的语句可以组合; 模块的元素只与该模块功能相关,模块之间按照顺序使用其输出结果作为输入结果
多线程介绍
更多1.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
2.http://www.cnblogs.com/xingele0917/p/3623162.html
Java提供了线程类Thread来创建多线程的程序。其实,创建线程与创建普通的类的对象的操作是一样的,而线程就是Thread类或其子类的实例对象。每个Thread对象描述了一个单独的线程。要产生一个线程,有两种方法:
◆需要从Java.lang.Thread类派生一个新的线程类,重载它的run()方法;
◆实现Runnalbe接口,重载Runnalbe接口中的run()方法。
为什么Java要提供两种方法来创建线程呢?它们都有哪些区别?相比而言,哪一种方法更好呢?
在Java中,类仅支持单继承,也就是说,当定义一个新的类的时候,它只能扩展一个外部类.这样,如果创建自定义线程类的时候是通过扩展 Thread类的方法来实现的,那么这个自定义类就不能再去扩展其他的类,也就无法实现更加复杂的功能。因此,如果自定义类必须扩展其他的类,那么就可以使用实现Runnable接口的方法来定义该类为线程类,这样就可以避免Java单继承所带来的局限性。
还有一点最重要的就是使用实现Runnable接口的方式创建的线程可以处理同一资源,从而实现资源的共享.
线程的状态:
- 创建:已经有Thread实例了, 但是CPU还有为其分配资源和时间片。
- 就绪:线程已经获得了运行所需的所有资源,只等CPU进行时间调度。
- 运行:线程位于当前CPU时间片中,正在执行相关逻辑。
- 休眠:一般是调用Thread.sleep后的状态,这时线程依然持有运行所需的各种资源,但是不会被CPU调度。
- 挂起:一般是调用Thread.suspend后的状态,和休眠类似,CPU不会调度该线程,不同的是,这种状态下,线程会释放所有资源。
- 死亡:线程运行结束或者调用了Thread.stop方法。
创建并运行线程:
- Thread()或者Thread(Runnable):构造线程。
- Thread.start:启动线程。
- Thread.sleep:将线程切换至休眠状态。
- Thread.interrupt:中断线程的执行。
- Thread.join:等待某线程结束。
- Thread.yield:剥夺线程在CPU上的执行时间片,等待下一次调度。
- Object.wait:将Object上所有线程锁定,直到notify方法才继续运行。
- Object.notify:随机唤醒Object上的1个线程。
- Object.notifyAll:唤醒Object上的所有线程。
线程安全与锁
更多1.https://www.cnblogs.com/zhaojinhui/p/5526988.html
2.https://www.cnblogs.com/lyjblogs/p/7888646.html
3.https://www.cnblogs.com/jalja/p/5887192.html
由于考虑到多个线程同时抢占资源时,会发生冲突,这叫做线程安全问题,即我们需要避免多个线程同时访问或修改同一资源,其实现机制为同步与互斥。避免冲突的方法就是对临界资源或者临界区加锁。
锁方法:
1.同步方法
1 在方法声明上加上synchronized
2
3 public synchronized void method() {
4 可能会产生线程安全问题的代码
5 }
同步方法中锁的对象是 this(即调用者对象),并不一定是方法中用到的对象数据!
2.静态同步方法
1 在方法声明上加上static synchronized 2 3 public static synchronized void method() { 4 可能会产生线程安全问题的代码 5 }
静态同步方法中的锁对象是 类名.class(因为在加载类文件的时候,静态同步方法由于是静态的也被加载进内存了,类名.class的加载优先级高于静态方法)
3.同步代码块
1 在需要同步的代码外面包上一个synchronized 2 3 synchronized(Object o) { 4 可能会产生线程安全问题的代码 5 }
同步代码块中锁的对象可以是任意对象(即Object o)
锁对象:
this指当前调用者对象,一般情况下,普通方法块中是省略的(在构造方法中没有省略)。
死锁:
当前线程正在访问共享对象时,由于一个确定的条件在某情况下不会发生,则该线程不释放锁,那么其余线程无法获得锁,也无法运行线程,多线程就会一直陷入等待中,等待这个“永远不会发生”的条件,这就会造成死锁。
原语操作和其他锁:
常用的原语有信号量P/V操作、管程、共享内存等,是进程之间常用的通信的方式。具体参考操作系统或原语、信号量。
synchronized是不错,但它并不完美。它有一些功能性的限制:
- 它无法中断一个正在等候获得锁的线程;
- 也没有有权利得到锁,只能一直陷入等待;即使不想等待,也就没法得到锁;
- 同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。
获取锁的线程释放锁只会有两种情况:
1、获取锁的线程执行完了该代码块,然后线程释放对锁的占有。
2、线程执行发生异常,此时JVM会让线程自动释放锁。
其他锁:
Lock与synchronized对比:
1、Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。
2、synchronized不需要手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
所以lock是可以手动释放锁的。
java.util.concurrent.locks包中常用的类和接口:
1 public interface Lock { 2 //用来获取锁。如果锁已被其他线程获取,则进行等待。 3 void lock(); 4 // 当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态 5 void lockInterruptibly() throws InterruptedException; 6 //它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false 7 boolean tryLock(); 8 /*与tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。
如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。*/ 9 boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 10 //释放锁 11 void unlock(); 12 Condition newCondition(); 13 }
1、Lock与unlock
Lock用于获取锁,但lock不会主动释放锁(调用unlock释放),所以需要与unlock()配合使用。一般在使用Lock时必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
PS:同一个线程可以连续获得同一把锁,但也必须释放相同次数的锁。
2、获取锁等待时间tryLock(long time, TimeUnit unit)
如果你和朋友有约,在约定时间内对方未出现,我想你肯定会扫兴的离去。对于线程来说也应该时这样的,因为通常我们是无法判断一个线程为什么会无法获得锁,但我们可以给该线程一个获取锁的时间限制,如果到时间还没有获取到锁,则放弃获取锁。
ReentrantLock增加了锁:
1. void lock(); // 无条件的锁;
2. void lockInterruptibly throws InterruptedException;//可中断的锁;
解释: 使用ReentrantLock如果获取了锁立即返回,如果没有获取锁,当前线程处于休眠状态,直到获得锁或者当前线程可以被别的线程中断去做其他的事情;但是如果是synchronized的话,如果没有获取到锁,则会一直等待下去;
3. boolean tryLock();//如果获取了锁立即返回true,如果别的线程正持有,立即返回false,不会等待;
4. boolean tryLock(long timeout,TimeUnit unit);//如果获取了锁立即返回true,如果别的线程正持有锁,会等待参数给的时间,在等待的过程中,如果获取锁,则返回true,如果等待超时,返回false。
Condition的特性:
1.Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
2.Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。
如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。
线程调度
更多1.https://www.cnblogs.com/xingele0917/p/3979494.html
2.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
线程创建后,就要将它们运行起来,然而怎样调度怎样分配CPU给线程是一个问题。
调度策略
一般线程调度模式分为两种——抢占式调度和协同式调度。抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
---------------------
作者:超人汪小建(seaboat)
来源:CSDN
原文:https://blog.csdn.net/wangyangzhizhou/article/details/41122385
但实际上我们有时需要手动进行调整,就是按照自我的意愿去改变线程调度方法,让某一个线程具有优先权,每当该线程的条件满足就停止其它线程,而运行该线程。
调度优化及线程优先级
优先级在各种常见算法中并不少见,我们调度多线程可以利用优先级来确定谁先谁后(操作系统中进程调度也有优先级调度方法)。理所当然,优先级高的线程会抢占CPU运行。
1 Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量: 2 static int MAX_PRIORITY 3 线程可以具有的最高优先级,取值为10。 4 static int MIN_PRIORITY 5 线程可以具有的最低优先级,取值为1。 6 static int NORM_PRIORITY 7 分配给线程的默认优先级,取值为5。
线程切换
线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
线程等待与唤醒
更多1.https://www.cnblogs.com/xingele0917/p/3979494.html
2.https://www.cnblogs.com/yjd_hycf_space/p/7526608.html
wait与sleep
共同点:
1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
不同点:
1.每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
2. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
3. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
notify与notifyAll
notify()只能唤醒一个在等待该对象(锁住的对象)线程,而notifyAll()唤醒所有在等待该对象的线程。
Obj.wait(),与Obj.notify()/Obj.notifyAll()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。