Java多线程
进程:一个程序的执行过程叫进程
线程:进程的一条执行路径
多线程:进程的多条执行路径
线程的五种状态
实现方式之一
声明一个类,继承Thread类,重写run()方法,调用类的实例对象的start()方法。
public class Test extends Thread{
@Override
public void run (){
//线程执行代码块
}
}
//main方法
main(arg){
new Test().start(); //线程启动 ,如果调用的是run()方法,则没有多线程效果。
//中间可写主线程需要执行的代码
}
实现方式之二
声明一个类,实现Runnable接口,也需要重写run()方法,将类的实例对象通过参数传入Thread类的实例对象里,然后调用Thread类实例对象的start()方法。
public class Test implements Runnable{
@Override
public void run(){
//需要执行的代码块
}
}
main(arg){
Test t=new Test();
Thread th=new Thread(t);
th.start(); //线程启动
//主线程执行代码块
}
解决线程安全问题
由于CPU切换线程快速,多个线程使用共享资源时会造成“异常”场景或情况,这就是线程安全问题。
可以给会出现异常的代码块用同步锁修饰(Synchronized),而且需要指定同一把锁。任何对象都可以作为锁,同一把锁表示同一个对象或常量。同步锁只能同步方法和代码块。
被Synchronized修饰的代码块,只有代码块执行完毕后才会释放锁,另一个线程才能拿到锁进入代码块。
锁对象
什么是锁对象?
每个java对象都有一个锁对象
如何创建锁对象:
每个java对象都可以作为锁,可以使用this关键字作为锁对象,也可以使用所在类的字节码文件对应的Class对象作为锁对象
类名.class
对象.getClass()
Java中的每个对象都有一个内置锁,只有当对象具有同步方法或同步代码块时,内置锁才会起作用,当进入一个同步的非静态方法时,就会自动获得与类的当前实例(this)相关的锁。获得一个对象的锁也称为获取锁。
因为一个对象只有一个锁,所以如果一个线程获得了这个锁,使用这个锁的其他线程就不能获得了,直到这个线程释放(或者返回)锁。
1:只能同步方法(代码块),不能同步变量或者类
2:每个对象只有一个锁
3:类可以同时具有同步方法和非同步方法,不用同步的方法不要加锁。
4:一个线程获得了锁,其他线程就不可以进入该锁对应相关的同步方法或同步代码块。
5:如果类同时具有同步方法和非同步方法,那么多个线程仍然可以访问该类的非同步方法。同步会影响性能、甚至死锁(两个线程相互拿着另一个线程所需要的资源不释放形成死锁。),同步优先考虑同步代码块(作用范围更小、更灵活)。
6:如果线程进入sleep() 睡眠状态,该线程会继续持有锁,不会释放。
线程的通讯
线程间也需要通信,一种方式是多个线程通过共享资源,来进行数据的交换,另一种是线程间可以通过对应的方法进行通讯。
Synchronized能解决线程安全问题,但是不能解决存一次,取一次的问题,有可能会出现存一次,取两次等问题。这时就要了解到线程的等待与唤醒机制。相关方法有:
wait(): 告诉当前线程放弃执行权,并放弃监视器(锁)并进入阻塞状态,直到其它线程执行并持有了相同的锁并调用notify或notifyAll方法为止。
notify(): 唤醒使用一个锁中第一个调用wait()进入等待的线程,被唤醒的线程进入Runnable状态,等待cpu执行权。
notifyAll(): 唤醒使用同一监视器(锁)中调用wait的所有的线程。
存一次之后调用notify方法唤醒取一次,存一次调用wait进入等待。取一次拿到cpu执行权后,执行完毕调用notify唤醒取一次,取一次调用waiti进入等待。达到存一次取一次的效果。
小结:
1、线程间通信可以通过多个线程操作同一个资源进行数据交换,也可以通过锁对象进行线程之间交互,例如:锁对象.wait(),锁对象notify(),锁对象.notifyAll()都是对持有锁的线程的交互操作,一般使用在同步代码中,只有同步才具有锁。
2、一些方法定义在Object类中
等待和唤醒必须是同一个锁。而锁由于可以使用任何对象,故定义在Object类中。
3、wait() 和 sleep()有什么区别?
wait():释放资源,释放锁。是Object的方法
sleep():释放资源,不释放锁。是Thread的方法
4、定义了notify为什么还要定义notifyAll()
Notify:唤醒持有相关监视器(锁),并且第一个调用wait方法后处于等待状态的线程。
notifyAll:唤醒持有相关监视器(锁)的所有处于等待状态的其它线程。
结束线程
1.线程的run()执行完毕,线程自动结束。
2.线程的stop方法,但是会出现线程还没执行完毕,就结束了的问题,不建议使用。
3.用标记停止线程,给当前线程定义一个标记,通过另一个线程改变标记,达到停止当前线程的效果。
前台线程和后台线程
前台线程特点:
应用程序必须运行完所有前台线程才可退出,默认启动的线程是前台线程
后台(守护)线程:
隐藏起来默默运行的线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台(守护)线程在应用程序退出时都会自动结束。执行main方法的主线程就是一个前台线程,如果没有其它前台线程,main方法执行完成后程序将退出。
设置为后台线程:
setDaemon(boolean on)
使用注意:
必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。
可以使用isDaemon() 测试该线程是否为后台线程(守护线程)。
Thread的join方法
Thread的join方法:Join可以用来临时加入线程执行,例如:A,B两个线程交替执行,当A线程执行到了B线程Join方法时,A就会等待,等B线程都执行完A才会执行。带参数的join方法可以指定合并时间,有纳秒和毫秒级别。
ThreadLocal类
用处:保存线程的独立变量。对一个线程类(继承自Thread),当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。
实现:ThreadLocal都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是ThreadLocal放的是entry而不是entry的链表。功能还是一个map。)以线程对象本身为key,以目标为value。通过提供set(Object o)和get()方法来对map的数据进行操作。
jdk1.5后多线程锁
程序不止靠synchronized实现锁功能,jdk1.5后并发包中新增了java.util.concurrent.locks.Lock接口以及相关实现类来实现锁功能,更灵活、功能更强大、更面向对象。
lock接口方法:
void lock():获得锁。如果锁不可用,当前线程被禁用,进入休眠状态,直到获取锁。
void lockInterruptibly():获取锁,如果可用并立即返回。如果锁不可用,那么当前线程将被禁用以进行线程调度,并且处于休眠状态,和lock()方法不同的是在锁的获取中可以中断当前线程(相应中断)。
Condition newCondition():获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
boolean tryLock():只有在调用时才可以获得锁。如果可用,则获取锁定,并立即返回值为true;如果锁不可用,则此方法将立即返回值为false 。
boolean tryLock(long time, TimeUnit unit):超时获取锁,当前线程在以下三种情况下会返回: 1. 当前线程在超时时间内获得了锁;2.当前线程在超时时间内被中断;3.超时时间结束,返回false.。
void unlock():释放锁。
Lock接口重要实现类:
ReentrantLock:排他锁
ReadWriteLock接口重要实现类:
ReentrantReadWriteLock:读写锁。
读写锁维护了两个锁,一个是读操作相关的锁也称为共享锁,一个是写操作相关的锁称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的。)。在没有线程Thread进行写入操作时,进行读取操作的多个Thread都可以获取读锁,而进行写入操作的Thread只有在获取写锁后才能进行写入操作。即多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作。
jdk1.5后多线程新的实现方式
实现Callable接口
1.实现Callable接口,重写接口的call方法。
2.FutureTask实例包装Callable实例,FutureTask实现了RunnableFuture接口,而Runnable接口又继承了Runnable和Future接口。这也就使得FutureTask能够作为Thread构造方法的一个参数,又能通过get方法获得返回值。
3.创建Thread实例包装FutureTask实例,调用Thread实例start方法启动线程。
4.通过FutureTask的get方法来获取返回值。
核心代码
public class Mycallable implements Calllable<String>{
@Override
public String call() throws Exception{
//需要执行的代码块
}
main(){
Callable<String> c=new Mycallable(); //创建实例对象
FutureTask<String> f=new FutureTask<String>(c); //创建FutureTask
new Thread(f).start(); //启动线程,省略try-catch
String result=f.get(); //获取返回结果
}
}
使用线程池
1.使用Callable、ExecutorService和Future实现又返回结果的多线程。
2.Callable接口:无返回值的任务必须实现Runnable接口,有返回值的任务必须实现Callable接口,并重写call方法。
3.Future接口:用来获取Callable任务后结果,执行Callable任务后,可以获取一个Future的对象,在该对象是调用get方法就可以获取Callable的call方法的执行结果。
4.ExecutorService接口:通过Executors类获取线程池接口ExecutorService就可以实现有返回结果的多线程了。
核心代码
main(){
ExecutorService pool=null;
//定义连接数
int poolSize=3;
//定义线程任务数
int taskSize=5;
//创建一个线程池,定制线程数目为poolSize
pool=Executors.newFixedThreadPool(poolSize);
//创建Callable对象
Callable<String> c=new Mycallable(); //Mycallable为Callable实现类
//五个任务三个线程
for(int i=0;i<taskSize;i++){
//获取结果
Future<String> f=pool.submit(c);
sysout(f.get());
//线程池关闭
pool.shutdown();
}
}