java 多线程
java线程运行机制
java虚拟机中,执行程序是由线程完成的,每个线程都有独立的程序计数器(PC或rip
)和栈
栈中有三个区,局部变量区,操作数栈和栈数据区。
操作数栈是线程的工作区,用来存放运算过程产生的临时变量(怎么有种寄存器的感觉)。
栈数据区为线程执行指令提供相关信息,例如定位到堆区和方法区的特定数据(类成员变量),正常退出方法等(存放地址信息)。
每次java虚拟机启动一个虚拟机进程时,虚拟机都会创建一个主线程。该线程从main开始执行。
例如在一个类中定义了一个实例变量a,然后再main函数中调用了这个类的成员函数A(),这个函数会让成员变量加一。首先进入这个方法,然后发现a++;就去栈数据区取出a的地址,之后定位到堆区取出a进行操作。
- 方法区存放了字节码
- 堆区存放了线程所需要长时间保存的数据(类)
- 栈区存放了临时数据,栈帧和跳转到其他区域地址。
线程的创建和启动
我们现在免费有了一个主线程,如果我们还想要创建其他线程的话,有两种方式。
- 扩展java.lang.Thread类
- 实现Runnable接口
要注意一点,创建了线程并启动之后会有自己的栈区,堆区,方法区,也就是说可以把它看成一个新的程序,里面的变量都是最原始的,不要认为主线程中操作的变量还有用。但是有些时候也可以使用主线程的对象
一个线程只可以被启动一次。
扩展java.lang.Thread类
thread是线程,它的最主要的两个方法是:
- run(): 包含线程运行时执行的代码(相当于main方法)
- start(): 用于启动线程,不需要覆盖
start方法用来确定什么时候开始执行,可以在别的main函数中执行,例如:
public class Machine extends Thread |
注意,调用了start并不是指立刻执行这个线程,而是让这个线程进入就绪状态,如果想让这个线程执行可以使用yield。
实现Runnable接口
java不允许一个类继承多个类,所以一旦继承了Thread类,那么就不能继承其他类。所以说接口这时就体现出优越性了。定义如下:
public void run();
例:
public class Machine implements Runnable |
Thread构造方法中有Runnable接口的,这个时候Thread就掌管了run方法,只要使用start就可以启动。
主线程和用户自定义线程并发运行
并发运行指的是一个线程没有结束另一个线程开始执行,上面举的例子都是并发运行。
Thread中的currentThread()静态方法返回当前线程的引用,getname()返回当前线程的名字,main方法名字是main,用户创建的线程根据顺序从Thread-0,Thread-1一直往后排,可以用setName()设置名字。
例:
String name = Thread.currentThread().getName(); |
为了让每个线程轮流获得cpu,可以使用sleep(time)放弃cpu并睡眠若干时间。
多个线程共享一个对象的实例变量
例:
- 方法内部局部变量不共享
这是因为这些数据都是动态在栈中分配的,每个线程都有自己的堆栈。
- 成员变量,如果是指向同一个对象就共享
package file2; |
程序中主函数定义了Num对象的实例i,定义线程是传递到了Thread0和Thread1这样三个变量就共享了一个Num对象的实例。而线程Thread0和线程Thread1又有自己的私有变量sno,可以用来保存某一时刻的共享变量的数值。
(1)Java中判断对象是否为同一个对象使用地址判断的。地址相同就是同一个对象,上面的三个就是同一个对象。
(2)如果把上面的例子中共享的对象实例用基本数据类型替换是不行的。因为基本数据类型程序会自动的用默认值初始化,也就是申明和定义时一起的。此时在main函数中定义线程,传递的基本数据类型参数,只能是初始化线程中的另一个对象,而不是同一个对象。
也就是说,只有类才可以共享,并且用同一个实例启动多个线程的时候这个实例是共享的,并且也只有这个实例时共享的,如果在线程中创建的新实例也不是共享的。
线程状态转换
新建状态
也就是只有new没有start状态。
就绪状态
调用了start方法后,就进入了就绪状态。虚拟机会为他创建栈和rip,这个线程在运行池中等待cpu使用权。
运行状态
就是得到了cpu使用权,如果是多个cpu,那么可以同时运行多个线程。
阻塞状态
阻塞状态优先级比就绪状态低,阻塞状态过去后首先进入就绪状态然后进入运行状态。
- 如果调用了某个对象的wait()方法,那么会进入这个对象等待池中。
- 如果试图获得某个对象的同步锁(后面说)但是被其他线程使用时,会把这个线程放到这个对象的锁池中。
调用sleep。
死亡状态
当退出了run后,进入死亡状态。前面已经提到过一个线程不能呗start两次。可以用Thread的isAlive()方法判断这个线程是否活着。如果处于新建状态时,也是false。
线程调度
线程调度就是确定cpu的使用权在那个线程手上,cpu通常只有一个。有两种调度模型:分时调度和抢占型调度。
分时型调度就是让每个线程都执行一定时间。
抢占型是java虚拟机采用的方式。它是将所有线程规定一个优先级,优先让优先级高的线程运行,如果优先级都相同,那么随机选取一个线程。一旦抢到cpu,那么它将会一直运行直到被迫停止运行。
被迫停止运行的可能:
- 虚拟机让当前线程放弃cpu。
- 当前线程进入阻塞状态
- 线程运行结束
不同操作系统对抢占型的实现不同,有的是会一直让这个线程运行,有的是一段时间后停止运行。
调整优先级
可以使用Thread的setPriority(int)和getPriority()设置优先级,可以使用currentThread().setPriority(?)设置等级。
三个静态常量:
- MAX_PRIORITY 10级,最高等级
- MIN_PRIORITY 1,最低等级
- NORM_PRIORITY 5, 默认等级
如果将主线程优先级更改然后再创建其他线程,那么其他线程优先级也会更改。被创建者的默认优先级和创建者优先级保持相同。
注意,上面三个静态常量在不同操作系统中是不同的,例如windows只有七个优先级,所以最好只使用上面三种优先级。
Thread.sleep()线程睡眠
如果使用sleep方法,那么他会放弃cpu进入阻塞状态。sleep后面的数据是以毫秒做单位。完成之后也不是立刻重新开始执行,而是先进入就绪状态,如果没人和它抢cpu就开始执行。
例如: sleep(100);表示阻塞100毫秒
可以使用sleep让两个线程比较同步的执行。例如每个线程都会调用sleep(100),那么第一个线程调用睡眠后第二个线程开始执行,如果执行时间小于100ms那么第二个线程执行完后第一个线程仍在睡眠,如果大于100ms第一个线程会立刻开始执行。因为它已经睡了100ms进入就绪状态了(同样也是只有两个线程的情况)。
可以使用sleep.interrupt()中断睡眠
如果线程在睡眠时被中断,那么就会抛出InterruptedException,程序跳到异常处理代码块。
Thread.yield()线程让步
执行yield后,如果有相同或者更高优先级的线程在就绪状态,那么将会把当前线程放入运行池中并让优先级高的线程执行。
join()等待其他线程结束
当前线程可以调用另一个线程的join()方法,直到另一个线程结束这个线程才会又开始执行(进入就绪状态)。
如果加了参数,那么类似于sleep()将会休眠若干时间,不同的是join是给指定的线程执行。
后台线程
例如垃圾回收线程,只有当前台所有线程都停止之后,后台线程才会结束生命周期。
主线程默认是前台线程,前台线程创建的线程默认也是前台线程。
可以调用Thread的setDaemon(true)方法,把线程设置成后台线程。可以用isDaemon()判断是否是后台线程。
只有在线程启动前(使用start())之前才可以设置成后台。并且后台创建的线程还是后台线程。
定时器
java.util包中提供了定时器Timer,TimerTask类表示定时器执行的一项任务。例;
package usetimer; |
TimerTask类是一个抽象类,它实现了RUnnable接口。start()中匿名类继承了TimerTask类。
TImer(boolean isDaemon)可以把相关联的线程设置成后台线程。如果是true就是后台
schedule(TimerTask task, long delay, long period)用来设置定时任务。所以前面的匿名类就是用来启动定时任务的。delay是推迟多少毫秒之后执行,period是每次执行任务的间隔。其中delay只在第一次运行时有效。
还可以不要period参数,表示只执行一次。例如,timer.schedule(task, 10);
同一个定时器可以执行多个任务。
线程同步
有时候线程的抢占可能导致问题。例:
public class Machine implements Runnable |
上面这个程序本来是要一直输出零的,但是因为让给了t2执行,所以会输出1 2 2 3 …。这样就和原来的逻辑不符。
逻辑紧密相关的一组操作叫做原子操作,为了防止原子操作被打断,就提出了线程同步思想。
线程同步只有在两个线程使用同一变量时才会使用。如上面都使用了machine的a,另外一种情况是都使用了全局变量。
synchronized同步代码块
例:
public String pop() |
这样就设置了this对象的锁。
- 如果这个锁被其他线程占用,那么就会把该线程加入锁池中,进入阻塞状态
- 如果没有线程占用,那么他就会占用并执行代码块。
也可以这样写;
public synchronized String pop() |
注意,在同步代码块中同样也可以使用sleep和yield,同样也是把cpu给其他线程。只是如果其他线程正好碰到了同步代码块那么又要将控制权归还,
synchronized不会被继承,也就是说哪怕父类中写了synchronized如果子类没写也不是同步的。
同步代码块中应包含尽量少的操作,因为操作多了可能一线程要在这里工作很久,其他线程就会都进入锁池中无法工作,这样会给一些需要即时反应的线程带来麻烦
释放对象的锁
释放锁的情况:
- 执行完同步代码块,会释放锁
- 线程异常终止时
- 执行了锁所属对象的wait()方法,这个线程会释放锁,并进入等待吃
但是使用sleep或yield只会放弃cpu,并不会释放锁。还有suspend方法
死锁
死锁指的是a在等b锁释放,b在等a锁释放,这样就永运无法释放,最为关键的是虚拟机并不会检查这类问题,所以只有程序员自己注意。
线程通信
线程通信就是一个线程告诉另外一个线程某个信息,通过配合完成某件事。
java.lang.Object类中有两个用于线程通信的方法:
- wait() 释放对象的锁,然后把该线程放入等待池中,等待其他线程把它唤醒
- notify() 唤醒在等待池中的线程。随机选取等待池中的线程,并加入锁池中。
进入等待池后,锁和cpu全部放弃。如果使用notify也只是进入锁池,还要和其他线程争夺锁。
notifyAll() 唤醒所有在等待池中的线程。
注意:wait()方法必须放在一个循环中。因为在调用notify后并不是立刻可以得到执行,而是先要获得该对象的锁和cpu执行权限才可以运行。这个时候可能其他线程又将状态改变了,这时又要重新运行。
要注意的是想要唤醒等待池中的线程首先自己要掌握这个锁,也就是说必须在同步代码块中写notify,不然会报IllegalMonitorStateException错误。
public synchronized String pop() |
上面例子中,首先point是-1就要让一个线程处理,现在point是-1,然后一个线程调用wait()进入等待池,之后另一个线程操作了这个方法唤醒了该线程,但是这个时候线程并没有执行,而是先让其他的线程执行了一会才开始执行。但是其他线程执行过程中又把point变成-1了,这个时候处理线程已经结束,就会出现问题。
此外,wait必须写在synchronized中,不然运行时会报错。
中断阻塞
当线程A处于阻塞状态的时候,B调用A的interrupt()方法,那么A会发送一个InterruptedExecption。
实际上interrupt方法如果处理处于阻塞状态的线程(如wait,sleep,join等)才会抛出异常,实际上它是把一个中断线程的标志位设为true,因为处于阻塞状态,所以抛出异常,仅此而已。决定是否退出线程还是由我们自己决定的,如果我们在catch块中没有退出,那么还是会继续运行。
如果interrput一个正在处于运行的线程,那么只会把标志位设置成true,不会做其他事。
可以通过isinterrupt来看标志位是否变成true
例:public class Machine extends Thread
{
private int a = 0;
private Timer timer = new Timer(true);
public synchronized void reset()
{
a = 0;
}
public void run()
{
final Thread thread = Thread.currentThread();
TimerTask timerTask = new TimerTask()
{
publc void run()
{
System.out.println(thread.getName()+"has waited for 3s");
thread.interrupt();
}
};
while(true)
{
synchronized(this)
{
timer.schedule(timerTask, 3000);
try
{
this.wait();
}
catch(InterruptException e)
{
System.out.println(thread.getName);
return;
}
}
a++;
System.out.println("a="+a);
}
}
public static void main(String[] args)
{
Machine machine = new Machine();
machine.start();
}
}
这段程序作用是如果a>3,那么就把线程放入等待池,如果等待时间超过三秒,那么就抛出InterruptException信号从而中断线程。
线程控制
- start() 启动线程
- suspend() 使线程暂停
- resume() 使暂停的线程恢复运行
- stop() 终止线程
但是其实后面三种已经被废弃了,但是可以通过编程实现同样的功能
用编程方式控制线程
可以设置一个标志变量来表示现在的状态,假设标志变量有三个值。
- SUSP, 暂停状态
- STOP, 终止状态
- RUN, 运行状态
例:public class Control extends Thread
{
public static final int SUSP = 1;//设置静态变量的话所有类都可以看到
public static final int STOP = 2;
public static final int RUN = 0;
public int state = RUN;
public synchronized void setState(int state)
{
this.state = state;
if(state == RUN)
{
notify();
}
}
public synchronized boolean checkState()
{
while(state == SUSP)
{
try
{
System.out.println(Thread.currentThread().getName()+"wait");
wait();
}
catch(InterruptException e)
{
throw new RuntimeException(e.getMessage());
}
}
if(state == STOP)
{
return false;
}
return true;
}
}
通过上面的两个方法,就可以对线程进行控制,但注意这种控制不是实时的,也就是说即使执行了setState也不会立刻进入暂停状态,而是machine先获得cpu,开始执行checkState方法时才会进入暂停状态。
线程组
ThreadGroup类表示线程组,他可以对一组线程集中管理。用户创建的线程都属于某个线程组。
指定线程组: Thread(ThreadGroup group, String name)
如果线程A创建线程B且创建时没有指定线程组,那么会自动加入A的线程组中。一旦线程加入线程组,就不能退出。
用户创建的线程组都有父线程组,默认情况下,如果A创建了一个新线程组,那么A所在的线程组就是父亲线程组。
指定父亲线程组: ThreadGroup(ThreadGroup parent, String name)
可以使用activeCount()返回当前活着的线程,enumerate(Thread[] tarray)把或者的线程复制到tarray中。
处理线程未捕获的异常
如果线程没有捕获异常,那么虚拟机会找UncaughtExceptionHandler实例(这东西是个接口)。并且调用它的uncaughtException(Thread t, Throwable e)方法
设置异常处理类:
setDefaultUncaughtExcpetionHandler(Thread.UncaughtExceptionHandler eh)
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
第一个是设置默认异常处理器(静态方法),第二个是设置当前异常处理器(实例方法)。
ThreadGroup线程组实现了这个接口。每次出现未捕获异常时,先找当前线程的异常处理器,如果没找到就用线程组的异常处理器。
并且线程组的异常处理器还不是直接调用。如果这个线程有父线程,那么就调用父线程的。如果没有父线程,那么如果自己实现了这个接口,那么就用。如果没有,那么就打印调用堆栈的异常信息。
ThreadLocal类
这个类用来存放线程的局部变量。这些局部变量是每个线程独有的,不会共享。
主要有三个方法:
public T get(): 返回当前线程局部变量
protected T initialValue(): 返回局部变量初始值
public void set(T value): 设置局部变量
其中initialValue()只会在第一次调用get()或set()时才会被使用,并且只会执行一次。
concurrent并发包
在编写多线程程序时,既要考虑并发,又要防止死锁,还要考虑性能,难度很大。为了降低难度,增加了java.util.concurrent包。下面是包含的类和接口
Lock外部锁
这个主要用于线程同步。这是由类提供的锁,区别于对象的锁(可以叫内部锁)。他有几个方法。
- lock() 获得当前线程的锁,如果被占用,那么进入阻塞状态。这和内部锁是一样的
- tryLock() 试图获得当前线程的锁(看看现在锁是不是有人用),如果被占用,就返回false,否则返回true。
- tryLock(long time, TimeUnit unit),如果超过了设置时间没有获得锁,放回false。例如 tryLock(50L, TimeUnit.SECONDS)表示时间限制50s
unlock() 释放线程锁占用的锁。
Lock接口有一个实现类ReentrantLock,构造方法ReentrantLock(boolean fair).fair如果是真,那么会采用公平策略。公平策略是指让阻塞时间长的更有可能获得锁。这是以性能作为代价的。
例:
public class Sample
{
private final ReentrantLock lock = new ReentrantLock();//创建
public void method()
{
lock.lock();
try
{
//
}
catch
{
...
}
finally
{
lock.unlock();//释放锁
}
}
}外部锁可以用来弥补内部所的一些不足,我们可以直接用lock,unlock而不使用synchronized。
Condition 线程通信接口
java.lang.concurrent.locks.Conditon用于线程通信。Lock接口的newCondition()方法返回Condition的实例。
方法:
await() 和wait()相似
- await(long time, TimeUnit unit):和上面类似,如果超过时间线程没有被唤醒,返回false。
- signal(): 和notify()类似。
signalAll():和notifyAll()类似
Callable和Feture
这两个是用来做异步计算的。Runnable接口的返回值是void,如果其他线程需要这个线程的返回值怎么办呢?这个可以通过共享变量来实现,但是共享变量需要共享类,这又可能导致问题。所以这两个接口就是解决这类麻烦。
Callable接口:和Runnable接口类似,Runnable中的run()相当于Callable中的call()。但是call可以有泛型的返回值。此外,这个不能作为Thread类的参数。
Future接口: 保存运算结果,以下参数
- get():返回异步运算的结果。如果结果没有出来,当前线程就会被阻塞直到运算结束。
- get(long timeout, TimeUnit unit): 和第一个类似。只是如果超出时间还没有得到结果就会抛出TimeoutException。
- cancel(boolean mayInterrupt): 取消该运算,如果运算没有开始,就立刻取消。如果已经开始,如果mayInterrrupt为true,那么也取消。
- isCancelled():判断运算时候被取消
- isDone():判断运算是否已经完成。
FutureTask:这是一个适配器,同时实现Runnable和Future接口。还关联了一个Callable实例。FutureTask可以作为Thread类的参数。FutureTask类的构造函数可以带Callable的参数。
Callable myComputation = new ... |
例如:public class Machine implements Callable<Integer>
{
public Integer call()
{
int sum = 0;
for(int i=1; i<=100; i++)
{
sum+=a;
try
{
Thread.currentThread().sleep(20);
}
catch(Exception e)
{
e.printStackTrace();
}
}
return sum;
}
public static void main(Stirng[] args)
{
FutureTask<Integer> task = new FutureTask<Integer>(new Machine());
Thread thread = new Thread(task);
threadMachine.start();
System.out.println("从1到100的和"+task.get());//调用get返回值
}
}
通过线程池管理多个线程
Executor表示线程池,execute方法用来执行command的run()中指定的任务,线程会调度空闲的线程来执行该任务。到底什么时候执行,这是由cpu决定的。
- shutdown(): 预备关闭线程池。如果有任务提交上去,那么要等这些任务执行完后,才会关闭线程池,并且拒绝新任务的入内。
- shutdownNow(): 终止已经开始的任务,立刻关闭线程池
- isTermination(): 判断线程池是否关闭,如果关闭返回true
- awaitTermination(): 等待线程池关闭。
submit(Callable
Excutors中静态方法:
- newCachedThreadPool(): 创建有缓存的线程池,有任务才创建新线程,空闲的线程停留60s。
- newFixedThreadPool(int nThreads): 创建有固定数目线程的线程池,空闲线程一直保留
- newSingleThreadExecution(): 创建只有一个县城的线程池。这个与newFixedThreadPool(1)不同之处在于这个终止就终止了,Fixed的终止了还会创建一个新的。
- newScheduledThreadPool(int corePoolSize): 线程池会按时间计划创建任务。corePoolsize是线程最小数目。
- newSingleThreadScheduledExecutor(): 创建只有一个线程的线程池,这个线程池按计划进行任务。
public class Machine implements Runnable |
这里创建了5个线程给线程池,然后线程池中两个空白线程接到任务开始工作,工作完这两个线程后又执行下面两个线程,直到执行完5个线程。然后执行shutdown()关闭线程池。
BlockingQueue 阻塞队列
java.util.concurrent.BlockingQueue接口继承了java.util.Queue接口。BlockingQueue接口为多个线程同时操作一个队列提供了方案。
操作 | 抛出异常 | 放回特定值 | 线程阻塞 | 超时 |
---|---|---|---|---|
添加元素 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除元素 | remove() | poll() | take() | poll(time, unit) |
读取元素 | element() | peek() | 无 | 无 |
前面两个是队列的,后面两个是阻塞队列的。这个具体等到队列那里再说。这里先把几个BlockingQueue的实现类列一下:
- LinkedBlockingQueue类: 默认情况下,LinkedBlockingQueue的容量是没有上限的,也可以指定大小,这是基于链表的队列
- ArrayBlockingQueue类: ArrayBlockingQueue(int capacity, boolean fair)可以设定容量,并且可以选择是否采用公平策略。这是基于数组的队列。
- PriorityBlockingQueue:这是优先队列(堆)
- DelayQueue: 这个队列中存放的是延期元素。这些元素必须实现java.util.concurrent.Delayed接口。只有延期满的元素才可以被取出或者删除。