以下是我第一次对线程学习的过程所查阅的资料和自我总结,有许多的内容是摘抄其他的文献,还有一些是自己的理解(如有错误请帮我指出,我好纠正,谢谢)
认识线程
进程是程序一次动态过程,他经历代码加载,执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。
为什么会有线程概念出现?
60年代,在OS中能拥有资源和独立运行的基本单位是进程,然而随着计算机技术的发展,进程出现了很多弊端,一是由于进程是资源拥有者,创建、撤消与切换存在较大的时空开销,因此需要引入轻型进程;二是由于对称多处理机(SMP)出现(该处理出现表示了多个进程是可以共享同一资源的),可以满足多个运行单位,而多个进程并行开销过大,因此在80年代,出现了能独立运行的基本单位——线程(Threads)。将一个进程的资源给轻量级的线程,线程本身只拥有保证独立运行的资源。
一个进程有多个线程。
线程是能独立运行的基本单位。
多个线程访问统一进程的主存和其他资源。
在一个进程中可以有多个线程或所有线程并发运行。
为什么线程是“轻”量的,因为线程能快速地启动和关闭。
Java多线程实现
java.lang.Thread是一个负责线程操作的类,任何类只要继承了Thread类就可以成为一个线程的主类,同时线程类中需要明确覆写父类中run()方法,(方法定义public void run() ),当产生该类的对象时,这些对象就会并发执行run()方法中的代码。
class MyThread extends Thread{
private String title;
public MyThread(String tutle){
this.title = title;
}
public void run(){
//需要执行的方法;
for(int i=0; i<10; i++){
System.out.println(this.title+":"+i);
}
}
}
public class ThreadDeam{
public static void main(String[] args){
new Thread("线程A").start();
new Thread("线程B").start();
new Thread("线程C").start();
new Thread("线程D").start();
new Thread("线程E").start();
new Thread("线程F").start();
new Thread("线程G").start();
}
}
//程序结果多变
从运行结果来看并不是线程越靠前的先执行,对于他们来说都是同时进行的所以A~G线程是并发执行的,原因是:从执行结果来看后面的线程也有先执行的结果出现。
虽然多线程的执行方法都在run()方法中定义的,但是在实际进行多线程启动时并不能直接调用此方法。由于多线程需要并发执行,所以需要操作系统的资源调度才可以执行,这样对于多线程的启动就必须利用Thread类中的start()方法(方法定义:public void start())完成,调用此方法时会间接调用run()方法。
为什么不直接调用run()方法,而是调用start()方法去间接调用run()?
run()方法只是一个普通的方法,真正达到线程并发的是start()方法,该方法中的源码可以看到
执行start()方法的过程:
1、先判断该线程是否重复启动,线程对象中有一个(threadStatus(初始值等于0))成员变量代表线程的状态,当线程为未启动状态时,threadStatus=0,当执行start方法时最先判断threadStatus!=0 ,如果threadStatus不等于0,抛出异常IllegalThreadStateException(非法线程状态异常),表示该线程已经启动了不能再重复启动该线程对象,就会停止运行再次启动的start方法。
2、将该线程加入到线程组中/group.add(this),this表示该线程对象,group是ThreadGroup类的对象,其作用是:可以批量管理线程或线程组对象,有效地对线程或线程组对象进行组织管理,因为ThreadGroup类是有两个变量:一个是Thread[]线程数组、一个是ThreadGroup[]线程组数组,所以可以看出ThreadGroup是一个树型结构,在判断完是否重复启动线程后,将该线程对象放入线程组中进行统一管理。
3、执行start0()方法,该方法内部执行run()方法和变化threadStatus的值,因为该方法使用native修饰,所以无法看到内部结构,其作用就是开启一个新线程,让操作系统来决定先执行哪一个run()方法,所以start0()和run()的区别就是,start()会开启新的线程,而run()就只是执行一次普通方法,并没有开启线程。而由于CPU的处理速度快并且处理机制又是分时机制,所以多个线程看起来像同时运行,所以多个线程才能达到并发效果。
4、最终判断start0()方法是否中断或出现异常或直到线程周期结束都没有到就绪状态,如果发生中断异常,线程组将移除该线程,执行group.threadStartFailed(this)方法,而如果没有发生异常,该线程将保留在线程组中。
5、捕捉group.threadStartFailed(this)方法异常,该异常由Throwable类截取,但不会输出任何异常处理信息,不理睬该异常。
6、为什么在源码中用布尔型的started变量去判断start0()方法是否报错中断,而不是通过捕捉异常catch去处理,是用finally处理报错中断问题,我猜想是该异常没有一个异常类可以捕捉,只能通过finally的特性无论是否中断报错都会执行,从而达到异常处理的问题。
start()的源码结构:
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*
*此方法不用于由虚拟机创建/设置的主方法线程或“系统”组线程。 将来添加到这个方法中的任何新功 *能可能也必须添加到VM中。
*
*当new一个型线程对象时,threadStatus=0
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
/*什么也不做。如果start0抛出Throwable
它将被传递到调用堆栈*/
}
}
}
Runnable接口实现多线程
明明有通过继承Thread获取线程类,从而实现多线程,为什么又要使用接口来实现多线程,接口的作用本身可以实现标准定义和解耦和操作的,其次单继承局限使线程对象无法在继承其他的父类,所以通过继承去实现多线程会减小程序代码的可用范围。
其次JDK1.8引入Lambda表达式后Runnable接口变成了函数式接口,表明该接口可以用lambda表达式进行编写。
其实通过Thread的源码可以看出,该类也是继承了Runnable接口,从结构上可以看出Thread像一个代理类,
通过Runnable接口实现多线程的方式为:
1、创建一个普通类去实现Runnable接口,并且重写run()方法。
2、在main方法中创建一个普通类的对象,将该对象通过构造方法创建一个Thread对象,用Thread的对象调用start()方法,产生一个新的线程,从而实现多线程的操作。
代码操作过程:
class MyThreada implements Runnable{
private String title;
public MyThreada(String title){
this.title = title;
}
@Override
public void run() {
for(int i=0; i<10; i++){
System.out.println(this.title+":"+i);
}
}
}
public class ThreadDeam2 {
public static void main(String[] args) {
Thread thread1 = new Thread(new MyThreada("线程A"));
Thread thread2 = new Thread(new MyThreada("线程B"));
Thread thread3 = new Thread(new MyThreada("线程C"));
Thread thread4 = new Thread(new MyThreada("线程D"));
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
同时也可以通过Lambda表达式进行实现:
new Thread(()->{
//run()方法的内容;
}).start();
为什么Thread类的构造方法中有Runnable类的参数?
首先Thread本身就是实现了Runnable接口,这个可以从源码中看出,(public class Thread extends Object implements Runnable{}),其次接口可以有效的避免单继承所带来的局限性,所以Thread就是为了让其他的普通类能实现Runnable接口达到多线程效果,所以才定义了一个成员变量(private Runnable target)
定义了一个构造方法public Thread(Runnable target){this.target=target;},并且还重写了run()方法如下:
@Override
public void run(){
if(target != null){
target.run();
}
}
从源码中就可以明显看出target就是自定义的实现Runnable接口的普通类,而该类重写的run()方法被直接引用在Thread类的run()方法中,如此就明显可以看出真实的主题就只有target对象中的run()方法,而Thread类就是代理类,主要提供的代理就是start()方法,该方法负责开启一个新的线程。
怎样才算代理模式,Thread类,Ruannable接口,普通线程类之间是否就是代理设计模式?
代理类和真实类都实现一个公共的接口,代理类中引入了真实类的对象或真实主题,有这两个出现基本就是代理模式,由此可以看出Thread和MyThread都实现了Runnable接口,并且Thread类中也引入了target的run()方法,所以它们之间是代理设计模式。
并发访问的设计如何去实现?
多线程开发的本质就是多个线程可以进行同一资源的抢占与处理,那么如何描述多个线程抢占同一资源呢?
首先是谁提供资源,线程本身是有运行时资源的,所以该资源不算同一资源,那么只有线程的实现子类提供的资源和操作系统提供的资源才算同一资源了,而操作系统的资源我们无法去修改和创造,那么只有实现了Runnable接口的实现类中创造资源才能达到共享资源,而创建更多的线程对象,就能创建更多的新线程,从而使线程运行时只对实现类的资源进行并发访问。
那如何去实现呢?
创建一个实现Runnable接口的普通类,在该类中创建资源和对该资源获取的方法,创建多个Thread对象和一个普通类的对象,运行Thread对象。
class MyThreadb implements Runnable{
private int title = 5;
@Override
public void run(){
for(int i=0;i<100;i++){
if(this.title>0){
System.out.println("卖票,title="+this.title--);
}
}
}
}
public class ThreadDeam{
public static void main(String[] args){
MyThreadb thread = new MyThreadb();
new Thread(thread).start();
new Thread(thread).start();
new Thread(thread).start();
}
}
从实验可以看出,生成的新线程越多结果越混乱,线程越少越有序。从而可以看出这些线程都是并发运行的。
为什么要使用接口被类实现的方式来进行资源共享呢,而不能用直接继承Thread类的方式?
如果使用直接继承Thread类的方式,那么子类本身就具有创造一个新的线程的方法start(),而每创建一个新的线程就会创建几个新的资源,那么资源就无法实现共享,资源变成了线程独有的资源。所以使用接口的方式才能更加合理的获取同一资源。
Callable接口实现多线程
我们知道run()方法是没有返回值的,所以这导致了线程的操作结果无法获取,而为了弥补这个缺陷,Java中提供了Callable接口,Callable接口的call()方法和run()方法相同,但是他是有返回值的。
Callable接口的源码:
@FunctionalInterface
public interface Callable<V>{
V call() throws Exception;
}
该接口可以设置一个泛型,该泛型的数据类型就是返回值类型,这样操作是为了避免向下转型带来的安全隐患,如果没有泛型的话返回值类型只能用Object类才行,那么就需要多进行一个向下转型时的判断操作了,那么如果不是该类型的话就会有安全隐患,所以使用泛型是最好的选择。
那么有关Callable接口的关系图
从图片中可以看出Callable和线程类没有任何的关系,但是FutureTask类中有Callable接口为类型的成员变量以及以Callable为参数的构造方法,所以总的来说Callable接口只是一个普通有泛型的接口,重点在FutureTask类中的重写了Runnable接口的run()方法
FuntureTask源码有关Callable的重要部分:
public class FutureTask<V> implements RunnableFuture<V> {
private Callable<V> callable;
private Object outcome;
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
private V report(int s) throws ExecutionException {
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = v;
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
@Override
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
}
从构造方法可以看出,将实现Callable接口的对象从当参数,如果参数为空,就抛出空指针异常,不为空,就将对象充当参数给成员变量callable赋值。并且将state=NEW值(确保可调用对象的可见性),保证该变量一开始并不为空。
该FutureTask类继承了Runnable接口所以重写了run()方法,从方法源码中可以看出
1、callable变量赋值给了c,而result的结果等于c.call(),所以result是返回值,将这个返回值result充当参数赋给set()方法;
2、set()方法通过判断将形参v赋给成员变量Object类型的outcome;
3、get()方法执行report()方法返回泛型的结果,而report()方法是将outcome进行向下转型然后充当返回值
综上所述:FutureTask类的对象使用get()方法可以获取到线程的返回值,该返回值只有线程结束后才可以获取到,因为FutureTask类是实现了Runnable接口的,所以还是需要Thread类的start()方法才能开启新的线程实现多线程并发。get()方法是FutureTask类实现Future接口重写的方法。
而获取返回值的操作方法如下:
class MyThread implements Callable<String>{
@Override
public String call() throws Exception{
for(int i=0;i<10;i++){
System.out.println(i);
}
return "线程A";
}
}
public class ThreadDeam{
public static void main(String[] args){
FutureTask<String> task = new FutureTask<String>(new MyThread());
new Thread(task).start();
System.out.println(task.get());
}
}
多线程运行状态
以下代码是Thread类源码中的枚举类,该类代表线程的六种状态:
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
线程状态从源码中可以看出有六种状态,分别是
NEW:尚未启动的线程处于此状态。
RUNNABLE:在Java虚拟机中执行的线程处于此状态。该状态有两个子状态,分别是就绪态和运行态。
BLOCKED:被阻塞等待监视器锁定的线程处于此状态。
WAITING:正在等待另一个线程执行特定动作的线程处于此状态。
TIMED_WAITING:正在等待另一个线程执行特定动作达到指定等待时间的线程处于此状态。
TERMINATED:已经退出的线程出于此状态。
NEW(新建状态)
线程对象创建了,但还没有启动之前,都是新建状态。
当我们执行new Thread(target)操作时,jvm要为线程的执行做一些前期的准备工作,比如检查线程类是否已经被加载,解析和初始化过,接下来还要为对象分配空间并对空间初始化处理等,接下来还要对对象进行一些类的元数据信息,对象GC等的设置信息,当完成准备工作时线程才能进入到下一个Runnable可运行状态.
public class MyThread{
public static void main(String[] args){
Thread t1 = new Thread(new Runnable(){
@Override
public void run(){
Thread.sleep(3000);
}
});
System.out.println(t1.getState());
t1.start();
System.out.println(t1.getState());
}
}
程序结果:
NEW
Runnable
当业务需要频繁创建线程时,最好使用线程池,提高效率减轻jvm的压力。当然如果大量线程进行频繁的上下文切换(解释在下面),此时多线程效率会大大折扣。
RUNNABLE(可运行状态)
一个JVM中执行的线程处于这个状态中,等待JVM调度,可能在执行,也可能在等待。
当线程有资格运行,并且调用了start()方法,线程首先进入了可运行状态,这种可运行状态不一定被线程调度运行,也就是说调用start方法后,他依然可能存在可运行池中没有运行,也可能运行,无论是否运行,该状态都属于可运行状态。
注意:这里的等待是等待调度,等待的是系统资源,如IO,CPU时间片,与sleep,lock的等待有本质的差别。
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。
更复杂的可能还会加入优先级(priority)的机制。
这个时间分片通常是很小的,一个线程一次最多只能在 cpu 上运行比如10-20ms 的时间(此时处于 running 状态),也即大概只有0.01秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)
注意:如果期间进行了 I/O 的操作还会导致提前释放时间分片,并进入等待队列。又或者是时间分片没有用完就被抢占,这时也是回到 ready 状态。
这一切换的过程称为线程的上下文切换(context switch),当然 cpu 不是简单地把线程踢开就完了,还需要把被相应的执行状态保存到内存中以便后续的恢复执行。
显然,10-20ms 对人而言是很快的,
不计切换开销(每次在1ms 以内),相当于1秒内有50-100次切换。事实上时间片经常没用完,线程就因为各种原因被中断,实际发生的切换次数还会更多。
也这正是单核 CPU 上实现所谓的“并发(concurrent)”的基本原理,但其实是快速切换所带来的假象,这有点类似一个手脚非常快的杂耍演员可以让好多个球同时在空中运转那般。
时间分片也是可配置的,如果不追求在多个线程间很快的响应,也可以把这个时间配置得大一点,以减少切换带来的开销。
如果是多核CPU,才有可能实现真正意义上的并发,这种情况通常也叫并行(pararell),不过你可能也会看到这两词会被混着用,这里就不去纠结它们的区别了。
通常,Java的线程状态是服务于监控的,如果线程切换得是如此之快,那么区分 ready 与 running 就没什么太大意义了。
当你看到监控上显示是 running 时,对应的线程可能早就被切换下去了,甚至又再次地切换了上来,也许你只能看到 ready 与 running 两个状态在快速地闪烁。
现今主流的 JVM 实现都把 Java 线程一一映射到操作系统底层的线程上,把调度委托给了操作系统,我们在虚拟机层面看到的状态实质是对底层状态的映射及包装。JVM 本身没有做什么实质的调度,把底层的 ready 及 running 状态映射上来也没多大意义,因此,统一成为runnable 状态是不错的选择。
那么线程进入Runable状态就是线程对象执行start()方法后的状态
BLOCKED(锁阻塞状态)
一个线程的线程状态被阻塞等待监听器锁定,处于阻塞状态的线程正在等待监听器锁定进入同步代码块或同步方法,或者在调用Object.wait后重新输入同步代码块或同步方法。
解释:监听器锁(也就是我们常说的同步锁synchronized)用于同步访问,以达到多线程的互斥(解释在下面),所以一旦一个线程进入同步锁,在其之前,其他的线程想进去,就会因为获取不到锁而阻塞在同步块之外,这时的状态就是 blocked(锁阻塞状态)。
简而言之,就是线程A与线程B使用同一种锁,线程A进入Runnable状态,线程B就进入Blocked状态。
线程安全
在多线程中,每个线程的执行先后是由CPU来决定,并且是不可控的,而CPU的运行机制是分时机制,所以在没有同步锁的情况下,每个线程都有可能被执行,并且可能都只执行了一部分后,CPU权限就被其他线程获取。然而,线程的资源是共享的,一旦多线程对资源进行写操作,那就有可能带来资源的错误。从而达到线程安全问题。
举例:买票现象:当票的个数只有100个,而窗口有三个同时买票,使用多线程模拟该现象。
public class Title implements Runnable{
private int title = 100;
@Override
public void run() {
while(true){
if(title > 0){
System.out.println(Thread.currentThread().getName()+":"+title);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
title--;
}
}
}
}
public class ThreadDeam5 {
public static void main(String[] args) {
Title ti = new Title();
new Thread(ti,"窗口A").start();
new Thread(ti,"窗口B").start();
new Thread(ti,"窗口C").start();
}
}
从执行结果来看:会出现在不同窗口买同一张票,这就是线程安全问题的出现,而造成该问题的原因是;
当三个窗口同时买票(就是三个线程并发运行),三个线程都进入线程队列,处于Runnable状态,CPU处理器权限假设先被窗口B获取,当窗口B运行到(title–)之前CPU权限就被窗口A抢去,从而导致票数(title)还没有进行减减(–),所以票数还是100个。同理窗口C也可能抢到CPU权限,票数还是不变,所以就会出现两个或三个窗口买同一张票,出现票数重复的现象。这就是线程安全问题的出现。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说全局变量是安全的;若有多个线程同时进行写操作,一般都需要考虑线程同步,否则的化可能影响线程安全。
而解决这个问题的方法就是给线程内部加同步锁。
线程同步
当多个线程添加同步锁后,只要有一个线程获得该锁,其他线程只能让该线程执行完后,才能获取锁,再进行执行,而要达到这一条件,只有多个线程之间产生互斥现象才行。
互斥访问的举例:更衣室只有一个,有三个人同时想换衣服,想抢到的人先换,而在换衣服期间,其他人只能等待更衣室的人换完衣服,才能继续抢夺更衣室。而这三个人之间就是互斥现象。更衣室相当于同步锁,哪个线程先抢到锁,哪个线程先执行,只有执行完后,其他线程才能获取锁。
然而要发生互斥现象的表现就是多个线程是同一锁,对于同步代码块来说就是多个线程的锁是同一个对象,如此才能产生互斥现象,从而才能达到线程同步,做到线程安全。
从代码的结构来看是如下操作:
public class Title implements Runnable{
private int title = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(title > 0){
System.out.println(Thread.currentThread().getName()+":"+title);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
title--;
}
}
}
}
}
public class ThreadDeam5 {
public static void main(String[] args) {
Title ti = new Title();
new Thread(ti,"窗口A").start();
new Thread(ti,"窗口B").start();
new Thread(ti,"窗口C").start();
}
}
或者如下
public class Title implements Runnable{
private int title = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
synchronized(obj){
if(title > 0){
System.out.println(Thread.currentThread().getName()+":"+title);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
title--;
}
}
}
}
}
public class ThreadDeam5 {
public static void main(String[] args) {
Title ti = new Title();
new Thread(ti,"窗口A").start();
new Thread(ti,"窗口B").start();
new Thread(ti,"窗口C").start();
}
}
同步锁:对象的同步锁只是一个概念,可以想象为在对象上标记一个锁。
1.锁对象可以是任意类型。
2.多个线程对象要使用同一把锁。
注意:在任何时候,最多只允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程进入BLOCKED状态。
从代码上看待锁阻塞状态解析:
public class ThreadA implements Runnable{
Object obj;
public ThreadA(Object obj){
this.obj = obj;
}
@Override
public void run() {
synchronized(obj){
System.out.println("线程A开始");
System.out.println("线程A执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A结束");
}
}
}
public class ThreadB implements Runnable{
Object obj;
public ThreadB(Object obj){
this.obj = obj;
}
@Override
public void run() {
synchronized(obj){
System.out.println("线程B开始");
System.out.println("线程B执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B结束");
}
}
}
public class ThreadDeam6 {
public static void main(String[] args) {
Object obj = new Object();
ThreadA ta = new ThreadA(obj);
ThreadB tb = new ThreadB(obj);
Thread t1 = new Thread(ta);
Thread t2 = new Thread(tb);
t1.start();
System.out.println(t1.getState());
t2.start();
System.out.println(t2.getState());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(t2.getState());
}
}
执行结果:
RUNNABLE
线程A开始
线程A执行
RUNNABLE
BLOCKED
线程A结束
线程B开始
线程B执行
线程B结束
从结果中可以看出线程B进入BLOCKED状态.
所以进入阻塞状态(BLOCKED)是很容易的,只要线程加一个同步锁并产生跟互斥访问,就能发生线程阻塞。
synchronized关键字
synchronized 同步的缺点:
- synchronized关键字同步的时候,等待的线程将无法控制,只能死等。
- synchronized关键字同步的时候,不保证公平性,因此会有线程插队的现象,
同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行。
也就是说当一个线程访问同步方法时,其他线程访问这个方法将会被阻塞(等待锁)。
同步代码块
用关键字 synchronized 声明方法在某些情况下是有弊端的,比如 A 线程调用同步方法执行一个较长时间的任务,那么 B 线程必须等待比较长的时间。这种情况下可以尝试使用 synchronized 同步代码块来解决问题。
同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。
技巧
为了尽可能的保证程序的性能,所以使用了同步块,在进行输出语句的调用时,并不会将当前对象锁定。众所周知,Java 在 I/O 方面的处理是比较慢的,因此在同步的语句当中,我们应当尽量的将 I/O 语句移出同步块(当然还包括一些其它处理较慢的语句)。
一句话:把需要同步的代码块包起来,注意不要把耗时的操作放在同步代码块中。比如打印输出、IO 操作等等。
同步代码块在执行时,是锁定 object对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。
synchronized(非this对象 object)将 object 对象本身作为对象监视器
synchronized(非this引用对象 object),这个对象如果是实例变量的话,指的是对象的引用,只要对象的引用不变,即使改变了对象的属性,运行结果依然是同步的。
锁的是堆内存中的对象,而不是引用。
在定义同步代码块时,不要使用常量对象作为锁目标对象。比如字符串常量、整形等。
如果需要同步静态方法的话,可以使用同步代码块锁定Class对象。
使用同步代码块锁定Class对象的代码如下:
/**
* 同步方法 - static
* 静态同步方法,锁的是当前类型的类对象。在本代码中就是Test_02.class
*/
package concurrent.t01;
import java.util.concurrent.TimeUnit;
public class Test_02 {
private static int staticCount = 0;
public static synchronized void testSync4(){
System.out.println(Thread.currentThread().getName()
+ " staticCount = " + staticCount++);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void testSync5(){
synchronized(Test_02.class){
System.out.println(Thread.currentThread().getName()
+ " staticCount = " + staticCount++);
}
}
}
注意:当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
WAITING(无限等待状态)
一个正在无限等待另一个线程执行一个特别(唤醒,如:notify()或notifiAll())的动作的线程处于这一状态。
详细定义:
一个线程进入WAITING状态是因为调用了以下方法:
不带时限的Object.wait方法。
不带时限的Thread.join方法。
LockSupport.park
然后会等一个线程执行一个特别的动作,比如
一个调用了某个对象的Object.wait方法的线程会等待另一个线程调用此对象的Object.notify()或Object.notifyAll()。
一个调用了Thread.join方法的线程会等待指定的线程结束。
注意:一个线程中同步代码块使用Object对象锁并且使用了同一个Object对象的wait方法,那么线程执行到该wait方法后会释放该Object对象的同步锁,当该Object对象wait()方法被唤醒后,在wait方法后面的程序必须让线程需要再次获得obj锁,才能继续执行。
如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。
当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。
public class ThreadDeam7 {
//对象锁一
private static Object lock1 = new Object();
//对象锁二
private static Object lock2 = new Object();
private static Object lock3 = new Object();
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1) {
System.out.println("线程一拿到了lock1锁");
System.out.println("线程一准备获取lock2锁");
synchronized (lock2) {
System.out.println("线程一拿到了lock2锁");
try {
System.out.println("线程一释放了lock2锁");
//先让出lock1锁,不设置超时时间
System.out.println("大撒大撒");
lock1.wait();
System.out.println("dsdsd");
//唤醒lock1等待的线程
/*lock2.notify();*/
System.out.println("线程一运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
//睡眠一秒,让线程一能够成功运行到wait()方法,释放lock1锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("线程二拿到了lock1锁,开始运行");
System.out.println("线程二准备获取lock2锁");
//唤醒lock1等待的线程
System.out.println("线程二获取了lock2锁");
lock1.notify();
/*try {
//先让出lock1锁,不设置超时时间
lock2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}*/
synchronized (lock2) {
System.out.println("线程二拿到了lock2锁,开始运行");
System.out.println("线程二运行结束");
}
}
}
});
thread1.start();
thread2.start();
thread2.interrupt();
}
}
TIMED_WAITING(计时等待状态)
带指定的等待时间的等待线程所处的状态。一个线程处于这个状态是因为用一个指定的正的等待时间(为参数)调用了以下方法的其一:
Thread.sleep
带时限(timeout)的Object.wait
带时限(timeout)的Thread.join
LockSupport.parkNanos(占时不做了解)
LockSupport.parkUntil(占时不做了解)
Thread.sleep方法是独立的,跟锁没有任何关系,而线程在休眠的这段时间的状态就是TIMESD_WAITING状态,一旦时间过了,就会马上进入Runnable状态。
TERMINATED(终止状态)
原因有两种将会被终止:
1.run()方法正常退出而自然死亡
2.一个没有捕捉的异常而被终止的run()方法
多线程常用操作方法
线程本身属于不可见的运行状态,及每次操作的时间是无法预料的,所以如果要想在程序中操作线程,唯一依靠的就是线程名称,而要想取得和设置线程得名称可以使用以下方法。
Thread线程类本身的构造方法中就提供了设置线程名称 public Thread(Runnable target,String name)。
public final void setName(String name)
public final String getName()
由于线程的状态不确定,所以每次可以操作的都是正在执行的run()方法的线程实例,可以依靠Thread类的一下方法实现。
获取当前线程对象:public static native Thread currentThread()
在哪个线程中写Thread t=Thread.currentThread() 那么t就代表哪个线程。
如果在主线程中写该方法,也能获取到主线程的线程对象。
所有的线程都是在程序启动后在主方法中启动的,所以主方法本身也是属于一个线程,而这样的线程就称为主线程。
Thread类的有关init方法的源码
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
g.addUnstarted();
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
在Thread类中每一个构造方法都调用了init方法,来看init方法中使用了哪些操纵。
有时间再进行分析讨论
其主要作用是:分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,作为 group 所引用的线程组的一员,并具有指定的堆栈大小。
线程会自动命名线程名称,以Thread-数字,该数字是线程在线程组中最后添加一个线程的索引数。
所有的线程都是进程基础上的划分,如果说主方法是一个线程,那么进程在哪里?
答:每一个JVM运行就是一个进程:当用户使用Java命令执行一个类的时候就表示启动了一个JVM进程,而主方法是这个进程上的一个线程而已,而当一个类执行完毕后,此进程会自动消失。
通过分析发现实际开发过程中所有的子线程都是通过主线程来创建的,这样的处理机制主要是可以利用多线程来实现一些资源耗费较多的复杂业务。
当程序存在多线程机制后,就可以将一些耗费时间和资源较多的操作交由子线程处理,这样就不会因为执行速度而影响主线程的执行。
线程休眠
作用:当一个线程启动后会按照既定的结构快速执行完毕,如果需要暂缓线程的执行速度,就可以利用Thread类中提供的休眠方法完成,该操作会让出CPU的使用权,让其他线程先操作。
休眠方法如下定义:
pubilc static void sleep(long millis)throws InterruptedException
设置线程休眠的毫秒数,时间一到自动唤醒。
public static void sleep(long millis,int nanos)throws InterruptedException
设置线程休眠的毫秒数与纳秒数,时间一到自动唤醒。
一个线程有休眠时的整个线程的运行状态,new->Runnable->timed_waiting->Runnable->terminated
sleep()方法在源码中的定义是用native定义的,说明该操作不是Java自己实现的,而是通过操作系统调用暂停当前线程的
在进行休眠时可能会产生中断异常InterruptedException,中断异常属于Exception的子类,程序中必须强制性进行该异常的捕捉与处理,为什么要进行强制的捕捉与处理呢?线程中断会详细解释。
sleep()方法有一个重载方法,源码如下:
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
通过源码可以发现该方法是编写了纳秒级别的时间,但在最后还是调用了毫秒级别的sleep()方法,以500 000纳秒作为分割,大于这个值时,线程在millis的基础上多sleep 1毫秒,否则还是sleep millis毫秒,当然如果millis为0时而nanos不等于0,会是sleep 1毫秒。
需要注意的是:
sleep是帮助其他线程获得运行机会的最好方法,但是如果当前线程获取到的有锁,sleep不会让出锁。
线程睡眠到期自动苏醒,并返回到可运行状态(Runnable),不是运行状态。
优先线程的调用,现在苏醒之后,并不会里面执行,所以sleep()中指定的时间是线程不会运行的最短时间,sleep方法不能作为精确的时间控制。
sleep()是静态方法,只能控制当前正在运行的线程(示例就是这样调用的,因为类对象可以调用类的静态方法)。
线程从休眠的那一时刻到休眠时间结束都处于timed_waiting状态.
总的来说:
Thread.sleep(XXX) 他告诉操作系统“在未来的XXX毫秒时间内我不参与CPU的竞争”。
Thread.sleep(0)的作用是:触发操作系统立刻重新进行一次CPU竞争。
竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力,这样界面就不会假死在那里。
补充:1.在Windows原理层面,CPU竞争都是线程级的。
2.操作系统中,CPU竞争有很多种策略。Unix系统使用的是时间片算法,而Windows则属于抢占式的。 在时间片算法中,所有的进程排成一个队列。操作系统按照他们的顺序,给每个进程分配一段时间,即该进程 允许运行的时间。如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。调度程 序所要做的就是维护一张就绪进程列表,,当进程用完它的时间片后,它被移到队列的末尾。 所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU 。因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU 。 在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一 个总的优先级来。操作系统就会把 CPU 交给总优先级最高的这个进程。当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他。
线程中断
在Thread类提供的线程操作方法中有很多都会抛出InterruptedException中断异常,所以线程在执行过程中也可以被另一个线程中断执行。从理解上来说,当一个线程中有写try{}catch(InterruptedException){}方法时,那么在try方法里面就会有一个监听器时时刻刻监听中断状态(中断状态是每个线程对象从新建就有的,初始值为false),而当在此之前,该线程调用了interrupt()方法,中断状态变成了true,一旦监听器监听到中断状态为true时,就会捕捉InterruptedException(中断异常),在之后线程的状态是由线程本身自己来看。所以说,线程中断只是让线程出现InterruptedException异常,而且还是要有捕捉该异常的try{}中才会出现,因为没有使用try{}捕捉是不会有监听器去监听该异常的(监听该异常的方式就是查看中断状态是否是为true)。
所以interrupt()方法只会在有try{}catch(InterruptedException e){}的线程中进行中断。
线程中断方法如下:
public boolean isInterrupted() 判断线程是否中断
public void interrupt() 中断线程执行
线程中断的源码如下:
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0(); //改变中断状态
}
interrupt()方法只是改变中断状态,不会中断一个正在运行的线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。
更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException
I/O操作和同步方法synchronized
无法通过interrupt()
方法中断
public class test1 {
public static volatile boolean exit =false; //退出标志
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println("线程启动了");
while (!exit) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程结束了");
}
}.start();
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
exit = true;//5秒后更改退出标志的值,没有这段代码,线程就一直不能停止
}
}
使用 interrupt() + isInterrupted()来中断线程
**this.isInterrupted()😗*测试线程是否已经中断,但是不能清除状态标识。
public static void main(String[] args) {
Thread thread = new Thread() {
public void run() {
System.out.println("线程启动了");
while (!isInterrupted()) {
System.out.println(isInterrupted());//调用 interrupt 之后为true
}
System.out.println("线程结束了");
}
};
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
System.out.println("线程是否被中断:" + thread.isInterrupted());//true
}
总结:从源码分析和理解来看,中断的实际意义是,线程执行interrupt方法,先对线程内部是否有Object.wait,Thread.sleep,Thread.join方法正在执行进行判断,如果有这些方法,就抛出InterruptedException(中断异常)再执行interrupt0()方法,将中断状态变成true,如果没有就直接执行interrupt0()方法,将中断状态变成true,从而在线程执行一些方法的时候,这些方法会先对中断状态进行判断,再进行相关的对应操作,达到有备无患的效果,所以对线程来说这个中断方法是非常安全的。
线程强制执行
作用:多线程启动后会交替进行资源抢占与线程体执行,如果此时有一个线程异常的重要也可以强制执行。
在多线程并发执行中每一个线程对象都会交替执行,如果某个线程对象需要优先执行完成,则可以设置为强制执行,待其执行完毕后其他线程再继续执行。
线程强制执行:public final synchronized void join()throws InterruptedException
该方法的源码展示:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码分析可以看出:当join方法在哪个线程中调用,那么那个线程就会进入等待池中,但是她并不影响其他的线程,所以如果一个线程t1中在运行的时候执行t2.join()方法的话,那么线程t1将进入等待池,不会影响t2和其他线程的运行,只有t1线程进入等待池,并且执行此方法中会判断t2线程是否是活跃状态,如果是活跃状态(解释在下面),就让t1进入等待池,如果有时间限制,那么就会让t1等待millis时间。
源码中可以看到如果millis==0并且线程t2一直处于活跃状态的话线程t1将一直在等待池中。
从程序代码中论证:
public class MyThread extends Thread{
private String title;
public MyThread(String title){
this.title=title;
}
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println(this.title+":"+i);
}
}
}
public class ThreadDeam8 {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread("线程A");
MyThread t2 = new MyThread("线程B");
MyThread t3 = new MyThread("线程C");
t1.start();
t2.start();
t1.join();
t3.start();
}
}
程序其中一个结果:
线程A:0
线程B:0
线程B:1
线程B:2
线程B:3
线程B:4
线程B:5
线程B:6
线程B:7
线程B:8
线程B:9
线程A:1
线程A:2
线程A:3
线程A:4
线程A:5
线程A:6
线程A:7
线程A:8
线程A:9
线程C:0
线程C:1
线程C:2
线程C:3
线程C:4
线程C:5
线程C:6
线程C:7
线程C:8
线程C:9
从结果中可以看出线程c永远是前两线程执行完后,他才开始执行,原因很简单:
主线程开始运行,当主线程运行到t1.join()之前的时候,线程A与线程B都已经开始独立运行了,当运行t1.join时,获得了t1对象锁,然后对t1线程A的活跃状态进行判断,很显然线程A是活跃状态,所以会将主线程进行wait方法操作,使主线程进入等待池中,失去抢夺CPU的机会,所以会停止运行,从而导致了主线程无法运行到t3.start()方法,所以线程C就无法进入Ruannble状态,而对于线程A与线程B来说,并不影响它们,它们会一直运行直到线程运行结束,一旦线程运行结束线程的活跃状态将会变成false(并且当一个线程运行完后,那么该线程对象的notifyAll()方法也会执行),那么主线程将会退出等待池重新进行CPU的竞争,之后t3.start()方法也会执行,那么就会执行线程C,这就是为什么线程C一直是最后一个执行的原因。
总结来看:对某线程进行强制执行,并不是真的让该线程先执行或一直执行,而是让执行该操作的线程进入等待的状态(waiting或timed_waiting),比如:t1线程中执行t2.join()操作,则让t1进入等待状态,对t2或其他线程没有影响。
线程礼让
作用:多线程在彼此交替执行的时候往往需要进行资源的轮流抢占,如果某些不是很重要的线程抢占到资源但是又不急于执行时,就可以将当前的资源暂时礼让出去,交由其他线程先执行。
线程礼让:是指当满足某些条件时,可以将当前的调度让给其他的线程执行,自己在等待下次调度在执行。
public static void yield();
源码展示如下:
public static native void yield();
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。
总结:当线程内有此方法时,CPU会再重新选择一次,第二次选择可能还是此线程,所以线程礼让不一定有效。
线程优先级
作用:所有创造的线程都是子线程,所有的子线程在启动的时候都会保持相同的优先权限,但是如果现在某些重要的线程希望可以优先抢占到资源并且先执行,就可以修改优先级来实现。
在线程操作中,线程在运行前会进入就绪状态,那么此时会根据线程的优先级进行资源的调度,即哪个优先级别高哪个线程就有可能会先执行。
优先级是存在与Thread类中的静态常量
源码如下:
/**
* The minimum priority that a thread can have.
*/
public final static int MIN_PRIORITY = 1;
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
/**
* The maximum priority that a thread can have.
*/
public final static int MAX_PRIORITY = 10;
设置优先级的方法:
public final void setPriority(int newPriority) 设置线程的优先级
public final int getPriority() 获取线程优先级
主线程的优先级为NORM_PRIORITY
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。
通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread类的setPriority()实例方法来设定线程的优先级。
既然有1-10的级别来设定了线程的优先级,这时候可能有些读者会问,那么我是不是可以在业务实现的时候,采用这种方法来指定一些线程执行的先后顺序?
对于这个问题,我们的答案是:No!
Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定会采纳。而真正的调用顺序,是由操作系统的线程调度算法决定的。
Java提供一个线程调度器来监视和控制处于RUNNABLE状态的线程。线程的调度策略采用抢占式,优先级高的线程比优先级低的线程会有更大的几率优先执行。在优先级相同的情况下,按照“先到先得”的原则。每个Java程序都有一个默认的主线程,就是通过JVM启动的第一个线程main线程。
还有一种线程称为守护线程(Daemon),守护线程默认的优先级比较低。
如果某线程是守护线程,那如果它的非守护线程结束,这个守护线程也会自动结束。
应用场景是:当所有非守护线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
线程的同步与死锁
在之前的解释中可以看到,线程安全问题,那么对于什么样的线程才需要同步呢?
何时需要同步 在多个线程同时访问互斥(可交换)数据时,应该同步以保护数据,确保两个线程不会同时修改更改它。 对于非静态字段中可更改的数据,通常使用非静态方法访问 对于静态字段中可更改的数据,通常使用静态方法访问。
1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有且仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法。
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的class对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对“原子”操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使。但是,一旦程序发生死锁,程序将死掉。 使用锁定还有一些其他危险,如死锁(当以不一致的顺序获得多个锁定时会发生死锁)。甚至没有这种危险,锁定也仅是相对的粗粒度协调机制,同样非常适合管理简单操作,如增加计数器或更新互斥拥有者。如果有更细粒度的机制来可靠管理对单独变量的并发更新,则会更好一些;在大多数现代处理器都有这种机制。
8、总而言之,一旦涉及到安全和线程问题,就一定需要进行同步处理。
那么线程同步处理有几种方式?
同步方法 :在方法前加一个同步的关键字snychronized,在加了此关键字的方法,在线程中会出现该方法获得这个线程的对象锁,而这个对象是调用这个方法的对象,如果是一个静态方法的话,那么就不是对象锁,而是类锁了。相关操作如下:
public class ThreadDeam9 {
public static synchronized void p(){
for(int i=0;i<10;i++){
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadDeam9 tt = new ThreadDeam9();
ThreadDeam9 ts = new ThreadDeam9();
Thread t1 = new Thread(new Runnable(){
public void run(){
System.out.println("线程A");
tt.p();
}
});
Thread t2 = new Thread(new Runnable(){
public void run(){
System.out.println("线程B");
ts.p();
}
});
t1.start();
t2.start();
}
}
同步代码块:synchronized(对象锁或类锁){}在同步代码块中的操作与其他同步锁相同的同步代码块产生互斥访问。
多线程像多个人在排队,而CPU相当于一个大礼堂,礼堂内只能一个人工作,而每一次选择人进去工作是随机的,所以线程运行相当于,礼堂选一个人工作,工作一段时间后把人拉出来放在队伍里面,又继续在队伍里面找个人进去工作,而同步指的是,在排队的人中有那么两个人只能在礼堂的一个特殊房间工作,而这个房间的钥匙只有一个,钥匙在门把手上,当这两人中的其中一个被选中进去工作后,这个人就自私的把钥匙拿在手里,这样这个人下次工作之前就能避免别人在进入到这个房间,所以礼堂在一段时间后把人拉出来放在队伍里,等选择另一个人的时候,发现礼堂里没有钥匙就无法工作,就会放弃这次的工作机会,她只有等到第一个拿到钥匙的人工作完后把钥匙还回那个房间,他才能去工作。
同步代码块中可以是任何的对象或类.class,但是要达到同步和互斥的效果,两个同步代码块的对象或类必须是一样的。
相关操作如下:
public class ThreadDeam10 implements Runnable{
Object obj = new Object();
public void run(){
System.out.println("线程执行");
synchronized(obj){
for(int i=0 ; i<10; i++){
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ThreadDeam10 tt = new ThreadDeam10();
new Thread(tt).start();
new Thread(tt).start();
}
}
Lock锁
通常我们在使用synchronized关键字的时候会遇到下面这些问题:
(1)不可控性,无法做到随心的加锁和释放锁。
(2)效率比较低下,比如我们现在并发的读两个文件,读与读之间是互不影响的,但如果给这个读的对象使用synchronized来实现同步的话,
那么只要有一个线程进入了,那么其他的线程都要等待。
(3)无法知道线程是否获取到了锁。
而上面synchronized的这些问题,Lock都可以很好的解决,并且jdk1.5以后,还提供了各种锁,例如读写锁,但有一点需要注意,使用synchronized关键时,无须手动释放锁,但使用Lock必须手动释放锁。下面我们就来学习一下Lock锁。
Lock是一个上层的接口,其原型如下,总共提供了6个方法:
public interface Lock {
// 用来获取锁,如果锁已经被其他线程获取,则一直等待,直到获取到锁
void lock();
// 该方法获取锁时,可以响应中断,比如现在有两个线程,一个已经获取到了锁,另一个线程调用这个方法正在等待锁,但是此刻又不想让这个线程一直在这死等,可以通过调用线程的Thread.interrupted()方法,来中断线程的等待过程
void lockInterruptibly() throws InterruptedException;
// tryLock方法会返回bool值,该方法会尝试着获取锁,如果获取到锁,就返回true,如果没有获取到锁,就返回false,但是该方法会立刻返回,而不会一直等待
boolean tryLock();
// 这个方法和上面的tryLock差不多是一样的,只是会尝试指定的时间,如果在指定的时间内拿到了锁,则会返回true,如果在指定的时间内没有拿到锁,则会返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 实现线程通信,相当于wait和notify,后面会单独讲解
Condition newCondition();
}
在使用Lock的时候,有一种固定的格式,如下:
public void run(){
Lock lock = new ReentrantLock();
lock.lock(); //上锁
try{
//需要执行的代码
}finally{
lock.unlock(); //释放锁
}
}
生产者与消费者
相关代码如下:
class Producer implements Runnable{
Message ms;
public Producer(Message ms){
this.ms = ms;
}
public void run(){
synchronized(ms){
for (int i = 1; i <= 20; i++) {
if(ms.getSum() != 0){
ms.notify();
try {
ms.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("给他赋值:"+i);
this.ms.setSum(i);
}
System.out.println("生产者结束");
ms.notifyAll();
}
}
}
class Consumer implements Runnable{
Message ms;
public Consumer(Message ms){
this.ms = ms;
}
public void run(){
synchronized(ms){
while(true){
if(ms.getSum() == 0){
ms.notify();
try {
ms.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("获取变量:"+ms.getSum());
if(ms.getSum() == 20){
break;
}
ms.setSum(0);
}
System.out.println("消费者结束");
}
}
}
public class Message {
private int sum ;
public void setSum(int sum){
this.sum = sum;
}
public int getSum(){
return this.sum;
}
public static void main(String[] args) {
Message ms = new Message();
Producer p = new Producer(ms);
Consumer c = new Consumer(ms);
Thread t1 = new Thread(p);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
Object线程等待与唤醒
重复操作的问题解决需要引入线程的等待与唤醒机制,而这一机制的实现只能依靠Object类完成。
三个方法如下:
public final void wait()throws InterruptedException 线程的等待
public final void notify() 唤醒第一个等待线程
public final void notifyAll() 唤醒所有的等待线程
一个线程可以为其设置等待状态,但是唤醒的操作有两种方法,所有等待的线程会按照顺序进行排列,如果使用了notify()方法,则会唤醒第一个等待的线程执行;而如果使用了notifyAll()方法,则会唤醒所有的等待线程,哪个线程优先级高,那个线程就有可能先执行。
wait()方法的源码解释如下:
public final native void wait(long timeout) throws InterruptedException;
当A线程调用某个资源的(例如a)wait的时候,需要a的资源,那么再调用a的wait时,A需要有a,再得到a后,如果是wait(long l)的话,那么A会自动放弃a资源l个时间,然后自己再来“抢”资源。如果是wait()的话,那么A会一直等下去,除非有线程来notify了它或者notifyAll了所有等待的线程,A才会接着“抢”。
按照Think in Java中的解释:“wait()允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变.而且只有在一个notify()或notifyAll()发生变化的时候,线程才会被唤醒,并检查条件是否有变.”
“wait()允许我们将线程置入“睡眠”状态”,也就是说,wait也是让当前线程阻塞的,这一点和sleep或者suspend是相同的.那和sleep,suspend有什么区别呢?
区别在于"(wait)同时又“积极”地等待条件发生改变",这一点很关键,sleep和suspend无法做到.因为我们有时候需要通过同步(synchronized)的帮助来防止线程之间的冲突,而一旦使用同步,就要锁定对象,也就是获取对象锁,其它要使用该对象锁的线程都只能排队等着,等到同步方法或者同步块里的程序全部运行完才有机会.在同步方法和同步块中,无论sleep()还是suspend()都不可能自己被调用的时候解除锁定,他们都霸占着正在使用的对象锁不放.
而wait却可以,它可以让同步方法或者同步块暂时放弃对象锁,而将它暂时让给其它需要对象锁的人(这里应该是程序块,或线程)用,这意味着可在执行wait()期间调用线程对象中的其他同步方法!在其它情况下(sleep啊,suspend啊),这是不可能的.
但是注意我前面说的,只是暂时放弃对象锁,暂时给其它线程使用,我wait所在的线程还是要把这个对象锁收回来的呀.wait什么?就是wait别人用完了还给我啊!
好,那怎么把对象锁收回来呢?
第一种方法,限定借出去的时间.在wait()中设置参数,比如wait(1000),以毫秒为单位,就表明我只借出去1秒中,一秒钟之后,我自动收回.
第二种方法,让借出去的人通知我,他用完了,要还给我了.这时,我马上就收回来.哎,假如我设了1小时之后收回,别人只用了半小时就完了,那怎么办呢?靠!当然用完了就收回了,还管我设的是多长时间啊.
那么别人怎么通知我呢?相信大家都可以想到了,notify(),这就是最后一句话"而且只有在一个notify()或notifyAll()发生变化的时候,线程才会被唤醒"的意思了.
因此,我们可将一个wait()和notify()置入任何同步方法或同步块内部,无论在那个类里是否准备进行涉及线程的处理。而且实际上,我们也只能在同步方法或者同步块里面调用wait()和notify().
这个时候我们来解释上面的程序,简直是易如反掌了.
synchronized(b){…};的意思是定义一个同步块,使用b作为资源锁。b.wait();的意思是临时释放锁,并阻塞当前线程,好让其他使用同一把锁的线程有机会执行,在这里要用同一把锁的就是b线程本身.这个线程在执行到一定地方后用notify()通知wait的线程,锁已经用完,待notify()所在的同步块运行完之后,wait所在的线程就可以继续执行。
需要注意的概念是:
调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在synchronized(obj) {…} 代码段内。
调用obj.wait()后,线程A就释放了obj的锁,否则线程B无法获得obj锁,也就无法在synchronized(obj) {…} 代码段内唤醒A。
当obj.wait()方法返回后,线程A需要再次获得obj锁,才能继续执行。
如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。
当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。
wait()和wait(0)等价。
有synchronized的地方不一定有wait,notify。
有wait,notify的地方必有synchronized.这是因为wait和notify不是属于线程类,而是每一个对象都具有的方法,而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。
如果要把notify和wait方法放在一起用的话,必须先调用notify后调用wait。
public final void suspend()暂停执行;
public final void resume() 恢复挂起的线程
为了停止线程,有许多种方式
如下:
public class ThreadDemo{
public static boolean flag = true;
public static void main(String[] args)throws Exception{
new Thread(()->{
long num = 0;
while(flag){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.println("中断异常");
}
}
}).start();
Thread.sleep(200);
flag = false;
}
}
守护线程
java中的线程分为两类:用户线程和守护线程。守护线程(Daemon)是一种运行在后台的线程服务线程,当用户线程存在时,守护线程也可以同时存在;当用户线程全部消失(程序执行完毕,JVM进程结束)时守护线程也会消失。
用户线程就是用户自己开发或者由系统分配的主线程,其处理的是核心功能,守护线程就像用户线程的保镖一样,如果用户线程一旦消失,守护线程就没有存在的意义了。在Java中提供有自动垃圾收集机制,实际上这就属于一个守护线程,当用户线程存在时,GC线程将会一直存在,如果全部的用户线程执行完毕了,那么GC线程也就没有了意义了。
- setDaemon(true)必须在调用线程的start()方法之前设置,否则会抛出IllegalThreadStateException异常。
- 在守护线程中产生的新线程也是守护线程
- 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。
- 如果同时开启用户线程跟守护线程,守护线程将与用户线程一起不停地运行下去
- 如果只开启了守护线程(即使有2个),当主线程结束的时候,守护线程也一起跟着结束了
- 如果是用户线程,即使主线程结束了,用户线程也是不会结束的
设置守护线程的程序
public final void setDaemon(boolean on) 设置为守护线程
public final boolean isDaemon() 判断是否为守护线程
public class ThreadDeam{
public static void main(String[] args){
Thread t1 = new Thread(()->{
for(int i=0; i<10; i++){
System.out.println(i);
}
System.out.println(Thread.currentThread.getName);
},"主线程");
Thread t2 = new Thread(()->{
for(int i = 0;i<10 ; i++){
System.out.println(i);
}
System.out.println(Thread.currentThread.getName);
},"守护线程");
t2.setDaemon(true);
t1.start();
t2.start();
}
}
Volatile关键字
在多线程编程中,若干个线程为了可以实现公共资源的操作,往往是复制相应变量的副本,待操作完成后再将此副本变量数据与原始变量进行同步处理,如果开发者不希望通过副本数据进行操作,而是希望可以直接进行原始变量的操作(节约了赋值变量副本与同步的时间),则可以在变量声明时使用volatile关键字。
大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。
也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码:
`i = i + 1;
当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。
比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗?
可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。
最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。
为了解决缓存不一致性问题,通常来说有以下2种解决方法:
1)通过在总线加LOCK#锁的方式
2)通过缓存一致性协议
这2种方式都是硬件层面上提供的方式。
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。
但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
1.在Java虚拟机的内存模型中多线程的操作过程如下:
如果CUP想要变量i,首先会从主存中获取变量i放入到CPU的高速缓存中,然后在从高速缓存中获取,再进行读/写操作,那么在多线程的情况下,会出现:线程A将变量从主存中放入到高速缓存,还没有进行其他操作后,CPU被其他的线程占用,然而每个线程都有自己的高速缓存,线程B也对变量i进行操作,那么线程B将从主存中获取变量i放入高速缓存,并进行了写操作,在返回给主存,之后再进行线程A的操作,但是线程A早就已经将变量i放入了高速缓存,那么变量i还没有进行线程B的过程,所以变量i还没有改变,那么就会出现数据不同步的现象。
而volatile修饰词对变量进行同步,如何进行的同步过程,
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
那么线程1读取到的就是最新的正确的值。
对于大部分的有关线程的基础我只了解这些,都是通过看书,网络了解的,一些是直接在网络摘抄下来的。如有摘抄错误请谅解,并提醒我,谢谢。