线程基础
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程是轻量级进程,是程序执行的最小单位。使用多线程而不是多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
线程生命周期:
线程的所有状态都在Thread中的State枚举中定义,如下所示:
public enum State{
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW状态表示刚刚创建的线程,还没开始执行。调用start()方法后,才开始执行。当线程执行时,处于RUNNABLE状态,表示线程所需的一切资源都已经准备好了。如果线程在执行过程中遇到了synchronized同步块,就会进入BLOCKED阻塞状态,从而暂停执行,直到获得请求的锁。WAITING和TIMED_WAITING都表示等待状态,它们的区别是WAITING会进入一个无时间限制的等待,TIMED_WAITING有时间限制。等待的线程在等待一些特殊的事件。比如,通过wait()方法等待的线程在等待notify()方法,而通过join()方法等待的线程则会等待目标线程的终止。一旦等到了期望的事件,线程就会再次执行,进入RUNNABLE状态。当线程执行完毕后,则进入TERMINATED状态,表示结束。(注意:从NEW状态出发后,线程不能再回到NEW状态,同理,处于TERMINATED的线程也不能再回到RUNNABLE状态)
Thread t = new Thread();
t.run();
-------这样并不能表示新建了一个线程,而是表示调用了当前线程。新建线程一定要通过其start()方法,在start()方法中新建线程和调用run()方法。
不要用run()方法来开启新线程,他只会在当前线程中,串行执行run()中的代码
Java中创建线程主要有三种方式:
一、继承Thread类创建线程类
二、通过Runnable接口创建线程类
三、通过Callable和Future创建线程
(还可以使用匿名内部类的方式创建线程)
创建线程的三种方式的对比
采用实现Runnable、Callable接口的方式创见多线程时,优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
使用继承Thread类的方式创建多线程时优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
终止线程:
Thread.stop()方法在结束线程时,会直接终止线程,并且会立即释放这个线程所持有的锁。而这些锁恰恰是用来维持对象一致性的。如果此时,写线程写入数据正写到一半,并强行终止,那么对象就会被破坏,同时,由于锁已经被释放,另外的读线程就可能读到这个被不完整的对象,这样数据就不一致了。因此,不要随便使用stop()方法来停止一个线程。
而要终止一个线程,可以用在程序中设置终止线程的条件。
线程中断:
线程中断并不会使线程立即退出,而是给目标线程发送一个中断的通知。至于目标线程接到通知后如何处理,则完全有目标线程自行决定。
与线程中断有关的三个方法:
public void interrupt() ; //中断线程 设置中断标志位
public boolean isInterrupted(); // 判断线程是否被中断
public static boolean interrupted(); //判断是否被中断,并清除当前中断状态
Thread t = new Thread(){
public void run(){
while(true){
if(Thread.currentThread().isInterrupted()){ //如果没有这里的中断处理的逻辑,那么即使中断了线程,也不会发生任何作用
System.out.println("Interrupted!");
break;
}
try{
Thread.sleep(2000);
}catch(InterruptedException e){
Thread.currentThread().interrupt(); //抛异常时,会清除中断标记,要下次循环才能捕获这个中断
}
Thread.yield();
}
}
}
t.start();
Thread.sleep(2000);
t.interrupt();
Thread.sleep()方法会让当前线程休眠,如果在线程休眠时被中断,就会抛出InterruptedException异常。让后它会清除中断标记。一般,在它清除了中断标记后,需要在异常处理中再次中断线程。
等待(wait)和通知(notify)
这两个方法属于Object类。当在一个对象实例上调用wait()方法后,当前线程会在这个对象上等待。比如,线程A 中,调用了object.wait()方法,那么线程A就会停止继续执行,而转为等待状态。若其它线程调用了object.notify() ,则会在所有等待的线程中随机选择一个,并将其唤醒。若其它线程调用了object.notifyAll()方法,那么所有等待的线程都会被唤醒。
Object.wait()方法必须包含在对应的synchronized语句中,无论是wait()和notify()都需要首先获得目标对象的一个监视器。
wait():获得目标对象的监视器 --->> 调用wait()方法 --->> 释放监视器 --->> 等待重新获得监视器 --->> 获得监视器后,执行wait()方法后的代码
notify() / notifyAll():获得目标对象的监视器 --->> 执行nofity() / notifyAll()方法
注意:Object.wait()和Thread.sleep()方法都可以让线程等待若干时间。但两者区别为:wate()方法可以被唤醒;wait()方法会释放目标对象的锁,sleep()方法不会释放任何资源。
join():等待线程结束(无时间限制,join(long millis)有时间限制)。join()的本质是让调用线程wait()在当前线程对象实例上。当线程执行完毕,被等待的线程会在退出前调用notifyAll()通知所有的等待线程继续执行
Thread.yield():当前线程让出CPU,然后再次和其它线程一起争夺CPU资源
Java的内存模型(JMM):JMM的关键技术点都是围绕多线程的原子性、可见性、有序性来建立的。
用volatile声明变量对于保证操作的原子性、可见性、有序性有很大帮助。
线程组: 用于管理一类功能相同的线程
ThreadGroup tg = new ThreadGroup(name);
Thread t1 = new Thread(tg, new ThreadGroupName(), "T1");
Thread t2 = new Thread(tg, new ThreadGroupName(), "T2");
t1.start();
t2.start():
守护线程(Daemon):
特殊的线程,系统的守护者,如垃圾回收线程、JIT线程。与之对应的可以称之为用户线程,用户线程是工作线程。当一个Java应用内,只有守护线程时,Java虚拟机就会自然退出。设置守护线程Thread.setDaemon(true),必须要在start()方法之前设置,否则会设置失败,抛出java.lang.IllegalThreadStateException异常,但程序却正常执行,被设置的线程还是被当作用户线程。
线程优先级:
优先级高的线程更有可能抢到资源,但这只是一个概率问题,并非每次都是高优先级的线程抢到资源。线程优先级的调度和底层操作系统有密切的关系,在各个平台表现不一样,并且优先级调度产生的后果不容易预测,无法精准控制,比如一个优先级很低的线程可能一直抢占不到资源,从而始终无法运行而产生饥饿。因此,在要求严格的场合,还是需要自己在应用层解决线程调度的问题。
synchronized的用法:
1.对给定对象加锁,进入同步代码前要获得给定对象的锁;
2.对当前实例加锁,进入同步代码前要获得当前实例的锁;
3.对当前类加锁,进入同步代码前要获得当前类的锁。
以下是错误地加锁的例子:
public class ThreadSafeDemo implements Runnable{
static int i = 0;
public synchronized void increase(){
i++;
}
public void run() {
for(int j=0; j<10000000; j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new ThreadSafeDemo());
Thread t2 = new Thread(new ThreadSafeDemo());
t1.start();
t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
错误原因:两个线程指向了两个不同的Runnable实例,因此在进入同步方法前,每个线程会加锁自己的Runnable实例,即是说两个线程使用的是两把不同的锁。因此,这样的代码就不能保证线程安全。
改正: 将increase方法加上static即可,也即时给类加锁
public static synchronized void increase(){
i++;
}
synchronized除了用于线程同步、确保线程安全外,还可以保证线程间的可见性和有序性。
Vector是线程安全的,而ArrayList则不是,所以在并行程序中可以用Vector代替ArrayList。
HashMap也不是线程安全的,在多线程时可以用ConcurrentHashMap代替HashMap