java多线程(一)—— 基础使用篇
1、线程的创建
1.1、继承Thread
public class main {
public static void main(String[] args) {
Thread t1 = new MyThread();//创建一个自己定义的线程
t1.setName("mythrea");//设置线程的名字
t1.start();//通过调用start()来进行线程的运行
}
}
class MyThread extends Thread{//继承Thread,且实现run()方法
@Override
public void run() {
System.out.println(currentThread().getName());
}
}
直接通过继承Thread,且重写run()方法就可以自定义一个新的线程类,然后通过创建一个线程对象,最后调用start()方法就开始创建一个新线程并且运行线程里run()方法中的内容。注意不是调用run(),如果调用run()方法只是一个简单的方法调用,而不会开启有个新的线程。
1.2、实现Runnable接口
public class main {
public static void main(String[] args) {
Thread t2 = new Thread(new ImpRunnableThread());
t2.setName("ImpRunnableThread");
t2.start();
}
}
class ImpRunnableThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
实现方式和第一种差不多,实现Runnable的接口,然后作为参数传给Thread.
1.3、FutureTask+Callable
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask<Integer>(new ImpCallableThread());
Thread t2 = new Thread(futureTask);
t2.setName("ImpCallableThread");
t2.start();
System.out.println(futureTask.get());//获得Callable的call()方法中的返回值,
//会阻塞当前进程,知道得到t2线程中的返回值。
}
}
class ImpCallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName());
return 1;
}
}
首先通过实现Callable的接口,然后在把这个实现类的一个对象作为参数传递给FutureTask,构造一个FutureTask对象,最终传递给Thread,创建一个新的线程。这种方式和上两种方式唯一的不同点是有返回值,最终通过FutureTask对象调用get()方法就能获得这个新线程中的返回值,调用get()方法会使得当前对象阻塞,直到获取到新的线程中的返回值。
2、线程的生命周期
java的线程的生命周期总的有六种状态:
- 初始状态(NEW):创建出一个线程对象,在调用start()方法前就是初始状态。
- 运行状态(RUNNABLE):java中将线程中的运行状态(RUNNING)和就绪状态(READY)合并成一个RUNNABLE运行状态,所以在java中线程的运行状态和就绪状态都叫运行状态RUNNABLE。
- 阻塞状态(BLOCKED):就是等待java中的synchronized锁资源时,就会变成阻塞状态,待获取到synchronized锁的使用权,就会重写变成运行状态(RUNNABLE)。
- 等待状态(WAITING):通过调用wait()、join()、park()方法使得当前线程进入到无限期的等待状态(WAITING),知道被程序主动的唤醒,即通过调用notify()、notifyALL()、unpark()方法对进程唤醒,最终才能进入到RUNNABLE运行状态。它是一种主动的阻塞情况。
- 超时等待状态 (TIMED_WAITING):它是一种有时间限制的等待状态,通过sleep(long)、wait(long)等方法设置等待的时间就进入到TIMED_WAITING超时等待状态,到了指定的时间后就自动变成RUNNABLE状态,不需要调用其他方法唤醒该线程。它也是一种主动的阻塞情况。
- 终止状态(TERMINATED):线程运行完了run()方法或者因为异常退出就会变成终止状态,这也代表了线程任务的完成。
java线程的生命周期如下图所示:各状态间的转化在图中就很好的说明了。
3、线程的简单使用
3.1、线程优先级
java中通过setPriority(int newPriority)方法来设置线程的优先级,默认优先级为5,最大优先级为10,最先优先级为1,对线程设置优先级,只是代表线程获得执行的概率大小,具体谁先执行谁后执行,最后由cpu决定。
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("setPriority()测试");
}
});
t2.setPriority(Thread.MAX_PRIORITY);//设置优先级
t2.start();
}
}
3.2、 线程休眠(sleep)
通过调用Thread.sleep(long millis)来使当前线程进行休眠,即使当前线程从RUNNABLE运行状态转化为TIMED_WAITING超时等待状态,待等到millis毫秒的时间后,该线程就会主动转变为RUNNABLE运行状态。
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);//当前线程睡眠,1秒钟以后转换为RUNNABLE状态,再运行之后的代码
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sleep()测试");
}
});
t2.start();
}
}
3.3 、线程礼让(yield)
java中通过调用 Thread.yield()函数,让当前获得cpu资源的线程放弃掉cpu资源的使用,让处于同一优先级的线程获得cpu资源的机会,其实就是将RUNNING状态转化为READY就绪状态。调用此方法只是一种建议而已,未必起作用。
3.4、线程中断(interrupt)
3.4.1、interrupt()
java线程中,调用interrupt()方法会使得当前线程的中断标志位变为true,默认为false,并且会抛出异常InterruptedException。所以该方法并不会让当前进程立即中断,而是会将中断标志位置为true,然后当检测到中断位为true的时候,抛出一个InterruptedException,仅仅如此,并不会真正的中断线程。
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程启动了");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("调用interrupt进入异常中");
e.printStackTrace();
}
System.out.println("会执行吗");
}
});
t2.start();
System.out.println("main线程");
t2.interrupt();
}
}
输出结果:
3.4.1、isInterupted()与interrupted()
这两个方法都能够获取到当前线程的中断状态的值,但是它们由两个区别:
- isInterrupted()是成员方法,也就是由当前线程对象进行调用,而interrupted()是静态方法,直接通过Thread.interrupted()调用。
- isInterrupted()方法只是获得当前进程的中断状态的值,而interrupted()方法得到当前线程的中断状态值后,会重置该线程的中断状态,即将该状态值置为false.
isInterupted()的代码实例:
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程启动了");
int i = 0;
while (!Thread.currentThread().isInterrupted()){//如果检测到当前线程的中断状态标志位为true则结束此循环,且不会改变标志位的值
System.out.println(i);
i++;
}
System.out.println("线程被中断了");
}
});
t2.start();
Thread.sleep(10);
System.out.println("main线程");
t2.interrupt();//将t2线程的中断状态标志位设置为true
}
}
运行结果:
interupted()的代码实例:
public class main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("没有调用interrup()方法:");
System.out.println("第一次:isInterrupted = "+Thread.interrupted());
System.out.println("第二次:isInterrupted = "+Thread.interrupted());
Thread.currentThread().interrupt();
System.out.println("调用interrup()方法后:");
System.out.println("第一次:isInterrupted = "+Thread.interrupted());//调用Thread.interrupted()后会将标志位置为false
System.out.println("第二次:isInterrupted = "+Thread.interrupted());
}
}
结果:
3.5、线程同步
在程序中,如果一个共享资源支持读写,然后多个线程对该资源进行操作时,由于多个线程的执行顺序的不同,就可能使得对于共享资源的使用变得混乱,即与预期的结果将不相同,也就是多个线程运行的随机性使得对共享资源的读写操作造成混乱,这就需要使用线程同步,使得多个线程对共享资源的读写按一定顺序进行,这样就避免造成共享资源的混乱。
举个列子:如下图所示,如果我们账户余额中原来有100元,然后现在有两个线程取钱和存钱同时对该资源进行操作,如果最终两个线程的执行顺序为1、3、4、2的顺序,会使得最终的余额为0,可是我们是存入100和取出100,按道理最终的余额应该为100,这就是因为多个线程执行的顺序的不确定性所导致。当我们取100元钱时,我们将余额减去100,并保存在变量t1中,即t1=0,此时线程1的cpu使用时间到了,有线程2获取到,他将余额加上100,并且写入内存中,此时余额为200,该线程结束,然后线程1又得到cpu的使用权,继续上一次的程序运行,将t1的值赋给余额,最终余额为0,这就使得最终的余额值和预期的值不一样。
归根结底,就是线程对一共享资源的操作不具备原子性,因为对于一个共享资源的操作可能有多个步骤,这多个步骤执行的过程中,共享资源可能会被其他线程操作改变,就会造成共享资源的混乱。线程同步就是使得每一个线程对共享资源的操作过程具有原子性,一个线程对共享资源操作完以后,下一个线程才有该共享资源的使用权,这就是线程的同步,一般通过加锁的方式来进行线程同步。
3.5.1、synchronized
synchronized是java中加锁的一种方式,通过它能够获得一个锁对象,要执行一段代码必须获得锁对象的引用,而同一时间只有一个线程获得这个对象锁的引用,该线程就可以在获得锁对象的过程中将需要同步执行的代码段执行完,这样就可以实现线程的同步了。关键字能修饰方法和对象,修饰方法时就代表着该方法在使用前必须获得锁对象,修饰对象时就是一段代码段在执行时需要获得对象锁。在java中,锁对象一般有两种,一种是普通对象,另一种是类对象,总的来说synchronized有四种使用方式。注意,要实现线程同步必须使用的是同一个锁对象。线程在拥有锁对象时,sleep()和IO操作都不会释放锁对象的引用,也就是在线程阻塞时也不会释放锁对象的引用,其他线程就必须得等待,就降低了线程的执行效率。
- 修饰成员方法:这种情况的锁对象就是this,也就是当前对象
- 修饰静态成员方法:锁对象是Ticket.class对象,即该锁的对象就是一个类对象。
- 修饰普通对象:就是new出一个对象,将该对象作为一个锁对象
- 修饰类对象:将类对象作为锁对象,注意一个类在jvm的内存结构中只有一个类对象。
3.5.2、Lock
通过Lock也能够实现同步锁,可是与synchronized不同,synchronized是java的内置对象,且释放锁时是自动的,不需要我们手动实现,而Lock只是一个接口,它的具体实现类是ReentrantLock,它释放锁时需要我们手动实现。Lock实现线程同步的过程可以分为两步,先获取到锁,然后再释放锁,获取锁的方式有四种:lock()、lockInterruptibly()、tryLock()、tryLock(long time, TimeUnit unit),释放锁通过unlock()来实现,Lock具体接口如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
四种锁的获取方式的简单说明:
- lock():普通的获取到锁,如果锁正在被其他线程使用则等待,否则就获取到。
- lockInterruptibly():可中断锁,如果锁没被使用则直接获取,否则就等待,但是此时该线程可以响应中断,可以通过thread.interrupt()去中断线程的等待。
- tryLock():获取到锁,如果获取到返回true,没有就返回false,没有获取到锁是不会进行等待的。
- tryLock(long time, TimeUnit unit):获取到锁,在time时长内获取到锁就返回true,否则返回false,也就是没有获取到锁只会等待time时长,否则就不等了。
由于使用此种加锁的方式需要自己去释放锁,为了防止发生异常以后不能够释放锁,所以一般实现方式是放在try、finally进行实现的,即:
lock lock = new ReentrantLock();
lock.lock();
try{
//同步代码块
}finally{
lock.unlock();
}
3.6、线程通信
在多个线程的运行过程中,一个任务或许是由多个线程协调完成的,而有时候这些线程之间的运行是有一定顺序的,这就使得需要线程之间能够进行通信,协调它们的运行顺序。
3.6.1、wait()/notify()/notifyAll()
该种方式只能用在synchronized方法体或代码块中,否则抛出IllegalMonitorStateException异常,且这几个方法是属于Object对象中的方法,而不是线程Thread对象。
- wait():必须在synchronized代码块中使用,通过锁对象调用此方法,会使得当前线程释放锁对象的引用,从而进入到等待阶段,即让线程从RUNNABLE状态转换为WAITING,只有待其他线程调用了此锁对象的notify()或者调用了notifyAll()方法才会唤醒对应的线程,。该方法还有一个重载方法wait(long
time),去设定等待的时间,就会使线程变为TIMED_WAITING状态,然后可以通过notify()、notifyAll()、到达time时间来唤醒线程。 - notify():唤醒当前对象调用wait()所在的线程,有可能有多个,具体哪一个由jvm决定,也就只有一个线程被唤醒,其他线程依然是WAITING状态,但是唤醒的线程还不能马上去运行,它需要去获得锁对象的使用才能运行,因为当前调用notify()的线程和被唤醒的线程是线程同步的,所以需要调用notify()方法的线程同步代码块执行完以后被唤醒的线程才能去获取到对象锁,才能进入到RUNNABLE状态,否则唤醒的线程会因为没有获得对象锁的引用而进入阻塞状态。
- notifyAll():使所有调用wait()方法的线程唤醒,能够竞争到对象锁的线程就转变为RUNNABLE状态,否则就进入到阻塞BLOCKED状态。
3.6.2、join()
join()是线程Thread中的一个方法,它也有一个重载函数join(long time),,它就是让调用该方法的线程进入WAITING或者TIMED_WAITING状态;threadA.join()是直到threadA线程执行完成,调用该方法的线程才能被唤醒,进而进入到阻塞状态或运行状态;而threadA.join(1000)是等待1秒以后就会被唤醒。
3.6.3、await()/signal()/signalAll()
这三个方法和wait()/notify()/notifyAll()是相似的,只是这三个方法服务于ReentrantLock实现的锁,而且它是通过如下方式使用,就是通过ReentrantLock对象产生一个Condition对象,然后通过这个对象将线程进入等待队列和进行相应,并且可以产生多个不同的condition对象,同一condition对象让不同线程进入同一个等待队列,不同的condition对象就会产生不同的等待队列,通过signal唤醒时也只是唤醒对应condition中的等待队列的线程。