进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位)
线程:线程是进程的一个实体,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位),它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口。
继承Thread类,start()方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。
所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。
Thread类实际上也是实现了Runnable接口的类。
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
4):线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
调整线程优先级:取值范围是1~10;static int MAX_PRIORITY 最高优先级:10;
staticint MIN_PRIORITY 最低优先级:1;static int NORM_PRIORITY 默认优先级:5。
setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
线程睡眠:Thread.sleep()方法调用目的是不让当前线程独自霸占该进程所获取的CPU资源,以留出一定时间给其他线程执行的机会。使线程转到阻塞状态。睡眠结束后,转为就绪(Runnable)状态。sleep()是一个静态方法,所以只对当前线程有效。
线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。
线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程,但有可能没有效果。
线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
为什么要用join()方法:如子线程要进行大量耗时运算,主线程将于子线程先结束,但如果主线程处理完其他事务后,需用到子线程的处理结果就需要等待子线程执行完后再结束。
线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。
sleep()和yield()的区别:
sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
sleep方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield()方法执行时,当前线程仍处在可运行状态,不可能让出较低优先级的线程些时获得 CPU 占有权。
如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。
sleep()和wait()的区别:
二者都可以暂停当前线程,释放CPU控制权,主要区别wait()在释放CPU同时,释放了对象锁的控制,使得其他线程可以使用同步控制块或者方法。
wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用;sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
wait通常被用于线程间交互,sleep通常被用于暂停执行。
interrupt():只是设置线程的中断标记,当对处于阻塞状态的线程调用interrupt方法时(处于阻塞状态的线程是调用sleep, wait, join 的线程),会抛出InterruptException异常,而这个异常会清除中断标记。在一个线程无时限sleep的时候也只有interrupt能够唤醒他。打断一个线程的Sleep时并不需要获得该线程的lock,与打断sleep不同的是,被打断的wait的线程在重新获得lock之前是不会抛出InterruptedException。
synchronized:多线程同步加锁,申请获取某个对象的锁
A.无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
B.每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
如果一个对象有多个synchronized方法,某一时刻某个线程已经进入到了某个synchronized方法,那么在该方法没有执行完毕前,其他线程是无法访问该对象的任何synchronized方法的
synchronized和java.util.concurrent.locks.Lock的异同
Lock能完成几乎所有synchronized的功能,并有一些后者不具备的功能,如锁投票、定时锁等候、可中断锁等等。lock接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁。
synchronized 是Java 语言层面的,是内置的关键字;Lock 则是JDK 5中出现的一个包,在使用时,synchronized 同步的代码块可以由JVM自动释放;Lock 需要程序员在finally块中手工释放。
如果需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,如何实现?
读写锁ReadWriteLock拥有更加强大的功能,它可细分为读锁和解锁。读锁可以允许多个进行读操作的线程同时进入,但不允许写进程进入;写锁只允许一个写进程进入,在这期间任何进程都不能再进入。
要注意的是每个读写锁都有挂锁和解锁,最好将每一对挂锁和解锁操作都用try、finally来套入中间的代码,这样就会防止因异常的发生而造成死锁得情况。
volatile有什么用
Volatile变量具有 synchronized 的可见性特性,但是不具备原子特性(原子性、可见性、有序性)。只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
A. 对变量的写操作不依赖于当前值。
B. 该变量没有包含在具有其他变量的不变式中。
volatile在多线程中是用来同步变量的。线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步。因此存在A和B不一致的情况。volatile就是用来避免这种情况的。volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(也就是上面说的A) 变量。
与 synchronized相比
一、volatile是变量修饰符,而synchronized则作用于一段代码或方法。一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的。因此不能将它cache在线程memory中。
二、volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。
为什么String要设计成不可变的?( 需要综合内存,同步,数据结构以及安全等方面的考虑)
1. 字符串常量池的需要。
2. 允许String对象缓存HashCode,保证了hash码的唯一性,因此可以放心地进行缓存。
3. 安全性:String被许多的Java类(库)用来当做参数, 如果不是固定不变的,将会引起各种安全隐患。
1、Java中多线程同步是什么?
在多线程程序下,同步能控制对共享资源的访问。如果没有同步,当一个Java线程在修改一个共享变量时,另外一个线程正在使用或者更新同一个变量,这样容易导致程序出现错误的结果。
2、Thread.start()与Thread.run()有什么区别?
Thread.start()方法(native)启动线程,使之进入就绪状态,当cpu分配时间该线程时,由JVM调度执行run()方法。start由本地方法实现,需要显示地被调用。
3、什么是死锁
死锁就是两个或两个以上的线程被无限的阻塞,线程之间相互等待所需资源。这种情况可能发生在当两个线程尝试获取其它资源的锁,而每个线程又陷入无限等待其它资源锁的释放,除非一个用户进程被终止。线程死锁可能发生在以下情况:
当两个线程相互调用Thread.join()
当两个线程使用嵌套的同步块,一个线程占用了另外一个线程必需的锁,互相等待时被阻塞就有可能出现死锁。
避免死锁的一个通用的经验法则是:
当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,再访问B和C。如把 Thread t2 = newThread(td2); 改成 Thread t2 = newThread(td1);
还有一种方法是对对象进行synchronized,加大锁定的粒度,如上面的例子中使得进程锁定当前对象,而不是逐步锁定当前对象的两个子对象o1和o2。
第三种解决死锁的方法是使用实现Lock接口的重入锁类(ReentrantLock)。比较轻量级的锁,代码行lock.tryLock()是测试对象操作是否已在执行中,如果已在执行中则不再执行此对象操作,立即返回false,达到忽略对象操作的效果。重入锁是一种递归无阻塞的同步机制
4、活锁、饿死
活锁指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败;两者一直谦让,都无法使用资源。活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。解决:避免活锁的简单方法是采用先来先服务的策略。
饥饿是指一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被调度执行的情况。解决:公平锁。
JavaAPI中线程活锁可能发生在以下情形:
当所有线程在程序中执行Object.wait(0),参数为0的wait方法。程序将发生活锁直到在相应的对象上有线程调用Object.notify()或者Object.notifyAll()。
当所有线程卡在无限循环中。
5、如何用Java实现阻塞队列?
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。(add offer put)(remove poll take)(抛出异常 返回特殊值 一直阻塞)
6、解决生产者/消费者问题的方法可分为两类:
1.采用某种机制保护生产者和消费者之间的同步;
2.在生产者和消费者之间建立一个管道。
第一种方式有较高的效率,并且易于实现,代码的可控制性较好,属于常用的模式。第二种管道缓冲区不易控制,被传输数据对象不易于封装等,实用性不强。
同步的核心问题在于:如何保证同一资源被多个线程并发访问时的完整性?
常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
在Java中一共有四种方法支持同步,其中前三个是同步方法,一个是管道方法。管道方法不建议使用,阻塞队列方法在问题4已有描述,现只提供前两种实现方法。
wait()/notify()方法
await()/signal()方法
BlockingQueue阻塞队列方法
PipedInputStream/PipedOutputStream
7、什么是原子操作,Java中的原子操作是什么?
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何线程切换。java.util.concurrent.atomic
8、什么是竞争条件?如何发现和解决竞争?
两个线程同步操作同一个对象,使这个对象的最终状态不明——叫做竞争条件。竞争条件可以在任何应该由程序员保证原子操作的,而又忘记使用synchronized的地方。
唯一的解决方案就是加锁。(synchronized、Lock)
9、Java中用到的线程调度算法是什么?
Java虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权。
Java虚拟机采用抢占式调度模型。是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
10、什么是线程组,为什么在Java中不推荐使用?
ThreadGroup线程组表示一个线程的集合。不使用是为了节省频繁创建和销毁线程的开销,提升线程使用效率。应该使用线程池(Thread Pool),可以重用已存在的线程,Executor 框架:这个框架可以用于异步任务执行。
11、为什么wait, notify 和 notifyAll这些方法不在thread类里面?
由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
12、Java中的fork join框架是什么?
forkjoin框架是JDK7中出现的一款高效的工具。一个巨大的优势是它使用了工作窃取算法,可以完成更多任务的工作线程可以从其它线程中窃取任务来执行。
13、ThreadPoolExecutor
Java通过Executors(ExecutorService)提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
14、条件阻塞Condition的应用
Condition将Object监视器方法(wait、notify和notifyAll)分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用。没有Lock就没法使用Condition,因为Condition是通过Lock来new出来的。
之前利用Condition来实现阻塞队列、线程间的唤醒,现在用BlockingQueue来实现。利用阻塞队列会阻塞一个线程的办法来实现两个线程之间交替执行。
自旋锁(Spin Lock)
为了让线程等待,我们只需让线程执行一个忙循环(自旋),占着CPU不放。避免了线程切换的开销,但它要占用处理器时间,因此如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次。
JDK 1.6中引入了自适应的自旋锁。自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
可重入锁
也叫做递归锁,指的是同一线程外层函数获得锁之后,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。
偏向锁(Biased Lock)解决无竞争下锁性能问题,是无锁竞争下可重入锁的简单实现
偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
整个synchronized锁流程如下:
1.检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2.如果不是,则使用CAS将当前线程的ID替换Mark Word,如果成功则表示当前线程获得偏向锁,置偏向标志为1
3.如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4.当前线程使用CAS将对象头的MarkWord替换为锁记录指针,如果成功,当前线程获得锁
5.如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
6.如果自旋成功则依然处于轻量级状态。
7.如果自旋失败,则升级为重量级锁。
轻量级锁就是为了在无多线程竞争的环境中使用CAS(比较并交换)来代替mutex(操作系统互斥),一旦发生竞争,两条以上线程争用一个锁就会膨胀重量级锁。
如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。
公平锁
是指多个线程在等待同一个锁时,必须按照申请锁的先后顺序来一次获得锁。好处是等待锁的线程不会饿死,但是整体效率相对低一些;公平锁可以使用new ReentrantLock(true)实现。
非公平锁
好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是非公平锁是可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
悲观锁(Pessimistic Lock):
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。
乐观锁(Optimistic Lock):
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。
读取频繁使用乐观锁,写入频繁使用悲观锁。在多线程的加锁机制中,JVM会首先尝试乐观锁,失败后才调用悲观锁。
共享锁
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
排它锁
如果事务T对数据A加上排它锁后,则其他事务不能再对A加任何类型的锁。获得排它锁的事务即能读数据又能修改数据。
读写锁
是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。具体使用方法这里不展开。
互斥锁
是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。
锁优化
高效并发:如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。
锁消除:是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除主要判定依据来源于逃逸分析的数据支持。
锁粗化:如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部,这样就只需要加锁一次就够了。