标签:函数 sig rac att svg images linu view 接口
这个事实上前面有提到过。FutureTask表示一个异步运算的任务。FutureTask里面能够传入一个Callable的详细实现类。能够对这个异步运算的任务的结果进行等待获取、推断是否已经完毕、取消任务等操作。当然,因为FutureTask也是Runnable接口的实现类,所以FutureTask也能够放入线程池中。我们知道在Java的新版中中引入了AIO模型,相对于之前的NIO,AIO编程提供了异步模型,假设对IO模型有疑惑,点这里:http://my.oschina.net/u/2288283/blog/625932. 在AIO编程过程中就会用到这个FutureTask。详细的使用方法我会在今后的博客中介绍一下。
这是一个比較偏实践的问题,这样的问题我认为挺有意义的。能够这么做:
获取项目的pid,jps或者ps -ef | grep java,这个前面有讲过
第一次看到这个题目。认为这是一个非常好的问题。非常多人都知道死锁是怎么一回事儿:线程A和线程B相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了,问一下怎么写一个死锁的程序就不知道了。这样的情况说白了就是不懂什么是死锁,懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的。
真正理解什么是死锁。这个问题事实上不难。几个步骤:
(1)两个线程里面分别持有两个Object对象:lock1和lock2。这两个lock作为同步代码块的锁;
(2)线程1的run()方法中同步代码块先获取lock1的对象锁。Thread.sleep(xxx),时间不须要太多。50毫秒几乎相同了,然后接着获取lock2的对象锁。
这么做主要是为了防止线程1启动一下子就连续获得了lock1和lock2两个对象的对象锁
(3)线程2的run()方法中同步代码块先获取lock2的对象锁,接着获取lock1的对象锁,当然这时lock1的对象锁已经被线程1锁,线程2肯定是要等待线程1释放lock1的对象锁的
以下演示一个死锁的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public class DeadLock { public static final Object lock1= new Object(); public static final Object lock2= new Object(); public static void main(String[] args){ Thread thread1= new Thread( new Runnable() { @Override public void run() { synchronized (lock1){ try { Thread.sleep( 1000 ); synchronized (lock2){ System.out.println( "线程1以获得锁1,正在获得锁2" ); } } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread thread2= new Thread( new Runnable() { @Override public void run() { synchronized (lock2){ try { Thread.sleep( 1000 ); synchronized (lock1){ System.out.println( "线程2以获得锁2,正在尝试获得锁1" ); } } catch (InterruptedException e) { e.printStackTrace(); } } } }); thread1.start(); thread2.start(); } } |
假设线程是由于调用了wait()、sleep()或者join()方法而导致的堵塞,能够中断线程,而且通过抛出InterruptedException来唤醒它;假设线程遇到了IO堵塞,无能为力,由于IO是操作系统实现的。Java代码并没有办法直接接触到操作系统。
前面有提到过的一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不须要进行额外的同步手段,提升了代码运行效率。比方String对象,在不同的线程中依旧能够“保持自我”,多么清高的String阿。。巴拉巴拉
多线程的上下文切换是指CPU控制权由一个已经正在执行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
假设你使用的LinkedBlockingQueue。也就是无界队列的话。没关系。继续加入任务到堵塞队列中等待运行。由于LinkedBlockingQueue能够近乎觉得是一个无穷大的队列,能够无限存放任务;假设你使用的是有界队列例如说ArrayBlockingQueue的话,任务首先会被加入到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
抢占式。
一个线程用完CPU之后。操作系统会依据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程运行。
这个问题和上面那个问题是相关的,我就连在一起了。因为Java採用抢占式的线程调度算法。因此可能会出现某条线程经常获取到CPU控制权的情况。为了让某些优先级比較低的线程也能获取到CPU控制权,能够使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
非常多synchronized里面的代码仅仅是一些非常easy的代码,运行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,由于线程堵塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码运行得非常快。最好还是让等待锁的线程不要被堵塞。而是在synchronized的边界做忙循环,这就是自旋。假设做了多次忙循环发现还没有获得锁,再堵塞,这样可能是一种更好的策略。详情见:http://my.oschina.net/u/2288283/blog/638969
Java内存模型定义了一种多线程訪问Java内存的规范。Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几部分内容:
(1)Java内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的。每次Java线程用到这些主内存中的变量的时候。会读一次主内存中的变量。并让这些内存在自己的工作内存中有一份拷贝。执行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完成之后,会将最新的值更新到主内存中去
(2)定义了几个原子操作,用于操作主内存和工作内存中的变量
(3)定义了volatile变量的使用规则
(4)happens-before。即先行发生原则,定义了操作A必定先行发生于操作B的一些规则,比方在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁unlock的动作一定先行发生于后面对于同一个锁进行锁定lock的动作等等,仅仅要符合这些规则。则不须要额外做同步措施,假设某段代码不符合全部的happens-before规则,则这段代码一定是线程非安全的
CAS。全称为Compare and Swap,即比較-替换。如果有三个操作数:内存值V、旧的预期值A、要改动的值B。当且仅当预期值A和内存值V同样时,才会将内存值改动为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合。这样才干保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说。永远是一个不会变的值A,仅仅要某次CAS操作失败,永远都不可能成功。
在Java的锁机制内部非常多都是借用了CAS锁来实现,当然CAS还是要靠硬件支持滴。
(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁觉得竞争不总是会发生,因此它不须要持有锁,将比較-替换这两个动作作为一个原子操作尝试去改动内存中的变量。假设失败则表示发生冲突。那么就应该有对应的重试逻辑。
(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁觉得竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁。就像synchronized,无论三七二十一。直接上了锁就操作资源了。
说点题外话,事实上乐观锁和悲观锁的问题在数据库中也非经常见,比方Mysql的锁问题,InnoDB默认的就是靠MVCC机制实现的乐观锁,悲观锁的话能够參考行锁。表锁等等来理解。
简单说一下AQS,AQS全称为AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。
假设说java.util.concurrent的基础是CAS的话,那么AQS就是整个Java并发包的核心了,ReentrantLock、CountDownLatch、Semaphore等等都用到了它。AQS实际上以双向队列的形式连接全部的Entry,例如说ReentrantLock,全部等待的线程都被放在一个Entry中并连成双向队列,前面一个线程使用ReentrantLock好了,则双向队列实际上的第一个Entry開始执行。
AQS定义了对双向队列全部的操作,而仅仅开放了tryLock和tryRelease方法给开发人员使用。开发人员能够依据自己的实现重写tryLock和tryRelease方法。以实现自己的并发功能。
老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下仅仅会被创建一次出来。
单例模式有非常多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全
只是对于单例的解释这里也仅仅是一个很粗略的,假设有兴趣的网友能够网上自行查找:怎样安全的创建单例模式,能够使用volatile呦。
。
Semaphore就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore有一个构造函数,能够传入一个int型整数n,表示某段代码最多仅仅有n个线程能够訪问,假设超出了n。那么请等待。等到某个线程运行完成这段代码块。下一个线程再进入。由此能够看出假设Semaphore构造函数中传入的int型整数n=1,相当于变成了一个synchronized了。
这是我之前的一个困惑,不知道大家有没有想过这个问题。
某个方法中假设有多条语句,而且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题。这非常好理解,可是size()方法明明仅仅有一条语句,为什么还要加锁?
关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:
(1)同一时间仅仅能有一条线程运行固定类的同步方法。可是对于类的非同步方法。能够多条线程同一时候訪问。
所以,这样就有问题了,可能线程A在运行Hashtable的put方法加入数据。线程B则能够正常调用size()方法读取Hashtable中当前元素的个数,那读取到的值可能不是最新的,可能线程A加入了完了数据。可是没有对size++。线程B就已经读取size了,那么对于线程B来说读取到的size一定是不准确的。而给size()方法加了同步之后。意味着线程B调用size()方法仅仅有在线程A调用put方法完成之后才干够调用,这样就保证了线程安全性
(2)CPU运行代码,运行的不是Java代码,这点非常关键,一定得记住。
Java代码终于是被翻译成汇编代码运行的,汇编代码才是真正能够和硬件电路交互的代码。即使你看到Java代码仅仅有一行,甚至你看到Java代码编译之后生成的字节码也仅仅有一行,也不意味着对于底层来说这句语句的操作仅仅有一个。一句”return count”如果被翻译成了三句汇编语句运行,全然可能运行完第一句,线程就切换了。
这是一个很刁钻和狡猾的问题。请记住:线程类的构造方法、静态块是被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个样例。如果Thread2中new了Thread1。main函数中new了Thread2。那么:
(1)Thread2的构造方法、静态块是main线程调用的,Thread2的run()方法是Thread2自己调用的
(2)Thread1的构造方法、静态块是Thread2调用的,Thread1的run()方法是Thread1自己调用的
同步块,这意味着同步块之外的代码是异步运行的,这比同步整个方法更提升代码的效率。请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点。虽说同步的范围越少越好。可是在Java虚拟机中还是存在着一种叫做锁粗化的优化方法。这样的方法就是把同步范围变大。
这是实用的,例如说StringBuffer。它是一个线程安全的类,自然最经常使用的append()方法是一个同步方法。我们写代码的时候会重复append字符串,这意味着要进行重复的加锁->解锁,这对性能不利,由于这意味着Java虚拟机在这条线程上要重复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作。将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就降低了加锁–>解锁的次数,有效地提升了代码运行的效率。
这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每一个人都能看到而且思考一下,由于这个问题很好、很实际、很专业。关于这个问题,个人看法是:
(1)高并发、任务运行时间短的业务,线程池线程数能够设置为CPU核数+1。降低线程上下文的切换
(2)并发不高、任务运行时间长的业务要区分开看:
a)假如是业务时间长集中在IO操作上。也就是IO密集型的任务。由于IO操作并不占用CPU。所以不要让全部的CPU闲下来,能够加大线程池中的线程数目,让CPU处理很多其它的业务
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务。这个就没办法了。和(1)一样吧,线程池中的线程数设置得少一些,降低线程上下文的切换
(3)并发高、业务运行时间长。解决这样的类型任务的关键不在于线程池而在于总体架构的设计。看看这些业务里面某些数据能否做缓存是第一步。添加server是第二步。至于线程池的设置,设置參考(2)。最后,业务运行时间长的问题,也可能须要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
=====================================================================
以上的问题都是參考了其它的博文,当然也增加了自己的一些观点,假设问题,欢迎指正。
这篇文章主要是对多线程的问...
2、Q:进程和线程的差别?
0条评论