线程
关于什么是线程,以及线程和进程之间的区别可以看之前的一篇文章《Linux下的TCP/IP编程—线程及多线程服务端》
线程的一些基础知识
线程是有优先级的。但是这种优先级并不可靠,主要取决于其JVM是如何进行实现的,所以说依赖于线程优先级的编程是不可靠的,程序的正确性不能依赖于线程的优先级,可以通过线程的priority属性来设置线程的优先级。
线程的状态(声明周期):
- NEW:初始状态,线程被构建,但是还没有调用start方法开始运行
- RUNNABLE:运行状态,Java线程将操作系统中的就绪和运行两种状态统称为运行
- BLOCKED:阻塞状态,表示线程阻塞于锁
- WAITING:等待状态,表示进入该状态的线程需要等待其他一些线程做出一些动作才能继续运行
- TIME_WAITING:超时等待状态,在等待状态的基础上加入了超时机制,在指定的时间自行返回
- TERMINATED: 终止状态,表示当前线程已经执行完毕
Daemon线程:这是一种支撑型线程,主要用于程序中后台调度以及支持性工作,主要特性是:当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出,而不考虑Daemon线程所处的状态,所以Daemon线程中的finally块并不可靠,有可能无法被执行到。可以通过线程的Thread.setDaemon(true)方法将线程设置为Daemon线程。
- 线程的构造:实现线程主要有三种方式,1>是继承自Thread类;2>是实现Runnable接口,将接口的实现类传入Thread中;3>是使用Callable和Future
代码示例:
/**
* 继承Thread类,并重写其run方法
* @author WQC
*
*/
class MyThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
}
}
MyThread myThread = new MyThread();
MyThread.start();
/**
* 实现Runnable接口,重写run方法,并在创建线程时传入Runnable的实例
* @author WQC
*
*/
class MyRunnable implements Runnable{
@Override
public void run() {
// TODO Auto-generated method stub
}
}
MyRunnable myRunnable = new MyRunnable();
Thread runnableThread = new Thread(myRunnable);
runnableThread.start();
/**
* 实现Callable接口并重写call()方法,并在使用时用FutureTask来包装Callable对象,在创建线程时将FutureTask传入Thread中
* @author WQC
*
*/
class MyCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
return null;
}
}
MyCallable myCallable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<Integer>(myCallable);
Thread callThread = new Thread(futureTask);
callThread.start();
- 线程的销毁:线程中可以有stop()方法来停止线程,但是这种方式已不推荐使用,因为不能保证线程的所占用的资源正常释放,通常未完成资源释放变已经停止了运行。最合适的方式是在线程中套用一个循环,通过循环的标志位来确定是否可以跳出循环,停止线程执行。
@Override
public void run() {
//通过标志位来结束线程,可以做一些资源回收操作
while(isStop){
//要执行的任务
}
}
多线程的优劣势
优势:
- 可以使用更多的处理器核心,由于现在处理器开始向多核发展,而不是一味的提升单核的频率,所以使用多线程技术可以极大的提高了处理器的利用率,发挥多核处理器的优势。
- 更快的响应时间,可以将多个关联程度不高的任务分给多个线程同时去处理,这样可以极大的加快处理速度,缩短响应时间,提升用户体验。
- 提供了更好的编程模型,使得开发人员能够专注于问题的解决而不是考虑如何将问题进行多线程实现。
劣势:
- 需要建立合理的多线程模型,考虑到多线程并发情况下程序的稳定性,在前期的规划工作很重要
- 要在合理的地方使用多线程方式,不然只会适得其反,降低程序效率。
- 使用多线程要合理的回收线程资源,不然容易导致死锁的产生。
线程间通讯
volatile关键字和synchronized关键字
volatile关键字:
volatile可以用来修饰类中的字段,告知程序任何对该变量的访问都需要从共享内存中获取,而对他的修改必须立即同步刷新回共享内存中,以此来保证多线程访问时该字段的可见性和一致性
public volatile boolean isStop = false;
synchronized关键字:
synchronized可以用来修饰方法或者是代码块,主要是保证多线程在同一时刻只能有一个线程处于synchronized方法或者是synchronized代码块中,保证了线程对变量访问的可见性和排他性。
/**
* 同步方法
* @param data
*/
public synchronized void setData(int data){
//需要同步的方法
}
/**
* 同步代码块
* @return
*/
public boolean isEmpty(){
synchronized (this) {
//需要进行同步的代码
}
return emptyFlag
}
等待/通知机制
主要工作原理是当一个线程A的执行时发现线程B还有一些工作尚未完成,此时可以在线程A中调用wait()方法来使线程A进入WAITING状态,此时线程A会释放所占用的资源。直到线程B完成了工作之后调用notify()或者是notifyAll()方法来通知线程A从WAITING状态转变为RUNABLE状态。
- wait():调用改方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,调用wait()之后线程会释放资源
- wait(long): 在wait()的基础上加入超时等待机制,时间单位是毫秒
- wait(long,int):时间粒度可以控制到纳秒
- notify():通知一个在对象上等待的线程,使其从wait()方法返回,前提是该线程获取到了对象的锁
- notifyAll():通知所有等待在该对象上的线程
通知/等待的经典模式
- 等待方:
- 获取对象的锁
- 如果条件不满足,调用对象的wait()方法,被通知之后仍要检查条件
- 条件满足则执行对应的逻辑
- 通知方:
- 获取对象的锁
- 改变条件
- 通知所有等待在对象上的线程
//等待者
synchronized(对象){
while(条件不满足){
对象.wait();
}
//对应的处理逻辑
}
//通知者
synchronized(对象){
改变条件
对象.notifyAll();
}
管道输入/输出
通过使用内存作为媒介来进行线程间的数据传输工作,主要有四种具体实现:PipedOutputStream,PipedInputStream(面向字节流),PipedReader,PipedWriter(面向字符流)
//声明输入输出的管道
PipedWriter out = new PipedWriter();
PipedReader in = new PipedReader();
//将管道连接起来
out.connect(in);
//输出线程开始输出
Thread printer = new Thread(new Print(in),"Printer");
printer.start();
//主线程作为接收线程开始接收
int recive = 0;
while((recive = System.in.read()) != -1){
out.write(recive);
}
//关闭管道
out.close();
-------
//输出线程
static class Print implements Runnable{
private PipedReader in;
public Print(PipedReader reader) {
// TODO Auto-generated constructor stub
this.in = reader;
}
@Override
public void run() {
// TODO Auto-generated method stub
int revive = 0;
try {
while((revive = in.read())!=-1){
System.out.println((char)revive);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Thread.join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待线程thread终止之后才从thread.join()返回也就是说通过Thread.join()方法可以实现线程之间的先后顺序,使得一个线程执行完一些准备工作之后另一个线程开始工作。
ThreadLocal
ThreadLocal即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值,可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法渠道原先设置的值。
public class ThreadLocalDemo {
public static void main(String[] args) throws Exception {
ThreadLocalDemo.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println(ThreadLocalDemo.end()+"");
}
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
protected Long initialValue() {
return System.currentTimeMillis();
};
};
public static final void begin(){
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end(){
return System.currentTimeMillis()-TIME_THREADLOCAL.get();
}
}
线程池技术
线程池的工作原理:
- 如果当前运行的线程少于corePoolSize,则创建新的核心线程来执行任务
- 若是运行的线程等于或者多于corePoolSize,则将任务加入到任务队列BlockingQueue中
- 如果无法加入任务队列BlockingQueue中,则创建新的非核心线程来处理任务
- 如果创建新线程将使当前运行的线程数超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法,执行拒绝策略
构造线程池:
//构造一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,handler)
向线程池提交任务:
向线程池提交任务有两种方式,一种是通过execute()方法,一种是通过submit()方法。
execute():这种方式没有返回值,所以无法判断任务是否执行完毕
submit():这种方式会返回一个Future对象,可以通过Future对象的get()方法来获取返回值,但是调用get()方法会阻塞当前线程,直到任务完成。
//通过execute提交任务
threadPoolExecutor.execute(command);
//通过submit提交任务
threadPoolExecutor.submit(command);
关闭线程池:
关闭线程池可以使用shutdown()方法,或者是shutdownNow()方法,两者的区别在于:shutdownNow方法是先将线程池的状态设为stop,然后尝试停止所有的线程,并返回正在执行任务的线程列表;shutdown则是将线程池的状态设置为stop,然后将停止所有未执行任务的线程。
//两中停止方式
threadPoolExecutor.shutdown();
threadPoolExecutor.shutdownNow();