Java多线程
先说进程和线程的区别:
- 进程是一个资源分配的最小单位,就是一个执行中的程序,它包括一个或多个线程。每个进程由3部分组成:CPU,data,code。每个进程都是独立的,保有自己的CPU时间,代码和数据。进程之间的切换开销很大。
- 线程是CPU调度的最小单元,一系列线程共享当前进行的进程资源,线程之间切换开销小 ,每个线程都有自己的运行栈和程序计数器。
并行和并发的区别:
- 并发,在单个处理器上采用单核执行多个任务即为并发。在这种情况下,操作系统的任务调度程序会很快从一个任务切换到下个任务,因此看起来所有任务都是同步的。并发概念重点是说一个事件触发机制,即同时存在多个任务,但不是同时运行。例如秒杀下,许多用户下单的操作,可以同时存在下单操作,但运行还是看服务器的调度。
- 并行,同一时间在不同计算机或处理器或处理器核心上同时运行多个任务,就是所谓的并行。这是一个真正的同时运行,并行概念重点是同时运行的多个任务。
Java中实现多线程
前面已经提到,如果是单个处理器的单个核心,那么多线程的实现仅仅是由于CPU调度很快,造成了同时运行的错觉,真正的多线程还是需要多个处理器核心。
1、实现多线程需要的类或接口
1)、Thread类:描述了执行并发Java应用程序的所有线程。实现方式为继承Thread类,并重写run()方法
public class ThreadDemo { public static void main(String[] args) { MyThread thread1 = new MyThread("A线程"); MyThread thread2 = new MyThread("B线程"); thread1.start(); thread2.start(); } } class MyThread extends Thread{ public MyThread(String name) { super(name); } @Override public void run() { for(int i = 0;i<5;i++) { System.out.println(Thread.currentThread().getName()+i); } } }
运行结果跟计算机的性能有关,结果可能A、B线程交替打印语句,实现了多线程。
2)、实现Runnable接口,重写run()方法
public class RunnableDemo { public static void main(String[] args) { TicketWindow tw = new TicketWindow(); Thread t1 = new Thread(tw,"张三"); Thread t2 = new Thread(tw,"李四"); t1.start(); t2.start(); } } //售票窗口 class TicketWindow implements Runnable{ //资源->100张票 private int ticket = 10; @Override public void run() { while(ticket>0) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"正在买第"+ticket--+"票"); } } }
与继承Thread相比,实现Runnable接口优点:
- 实现Runnable接口,还可以继承其他类。而第一种方式只能继承Thread类,有局限。
- Runnable接口的实现类对象相当于拥有一份相同资源,开启的多线程可以共享这份资源。
以上两种方式实现run方法时,只能抛出运行时异常,因为在接口中的run方法没有抛出任何异常。注意子类重写父类方法时只能抛出比父类抛出异常限定更小的异常。而且这两种run方法都不可以有返回值。
3)、实现Callable接口
public class CallableDemo { public static void main(String[] args) { MyCallable thread=new MyCallable(); FutureTask<String> future=new FutureTask<String>(thread); new Thread(future).start(); System.out.println("这是主线程begin!"); try { System.out.println("得到的返回结果是:"+future.get()); }catch(InterruptedException e) { e.printStackTrace(); }catch(ExecutionException e) { e.printStackTrace(); } System.out.println("这是主线程end!"); } } class MyCallable implements Callable<String>{ @Override public String call() throws Exception { try { Thread.sleep(500L); }catch(InterruptedException e) { e.printStackTrace(); } System.out.println("这是线程C"); return "thread C"; } }
Callable接口在guc包下,通过FutureTask线程结果处理类可以得到线程返回结果。Callable接口是一个函数式接口,重写call方法,call方法有返回值,且接口中的方法抛出了Exception异常,因此子类重写可以抛出任何异常。
2、线程的状态
Thread类中的States枚举中定义了线程的所有状态:
public enum State { /** * 新生线程,还未运行,仅仅由JVM分配了内存 */ NEW, /** * 可运行中线程的状态。处于可运行状态的某一线程正在 Java 虚拟机中运行,但它可能正在等待操作 * 系统中的其他资源,比如处理器。调用start方法后处于这种状态,又分为READY就绪状态和RUNNING * 运行状态,两者可以相互转换。 */ RUNNABLE, /** * 正在等待监视器锁的阻塞线程,只能重新进入就绪状态才有可能运行。 */ BLOCKED, /** *等待状态,当处于运行的线程调用无时间参数限制的方法,如wait()、join(),就转为等待状态 */ WAITING, /** * 定时等待状态,调用有时间参数限制的方法。 */ TIMED_WAITING, /** * 终止状态,线程run()、call()方法正常执行完毕或抛出一个未捕获的异常,线程就进入终止状态 */ TERMINATED; }
3、线程类的部分方法
public class ThreadMain{ public static void main(String[] args) throws ExecutionException { ThreadB threadb=new ThreadB(); for(int i=0;i<5;i++) { new Thread(threadb,"线程名称:("+i+")").start(); } Thread threadMain=Thread.currentThread(); System.out.println("这是主线程;"); System.out.println("返回当前线程的线程组中活动线程的数目:"+Thread.activeCount()); System.out.println("主线程的名称:"+threadMain.getName()); System.out.println("返回该线程的标识符:"+threadMain.getId()); System.out.println("返回该线程的优先级:"+threadMain.getPriority()); System.out.println("返回该线程的状态:"+threadMain.getState()); System.out.println("返回该线程所属线程组:"+threadMain.getThreadGroup()); System.out.println("测试该线程是否为守护线程:"+threadMain.isDaemon()); try { Thread.sleep(10000L); }catch(InterruptedException e) { e.printStackTrace(); } } } class ThreadB implements Runnable{ @Override public void run() { try { Thread.sleep(10000L); }catch(InterruptedException e) { e.printStackTrace(); } Thread curThread=Thread.currentThread(); String curThreadName=curThread.getName(); System.out.println("这是线程的名字:"+curThread.getName()); System.out.println("返回当前线程"+curThreadName+"的线程组中活动线程的数目:"+Thread.activeCount()); System.out.println("返回该线程"+curThreadName+"的标识符"+curThread.getId()); System.out.println("返回该线程"+curThreadName+"的优先级"+curThread.getPriority()); System.out.println("返回该线程"+curThreadName+"的状态"+curThread.getState()); System.out.println("返回该线程"+curThreadName+"所属的线程组"+curThread.getThreadGroup()); System.out.println("测试该线程"+curThreadName+"是否处于活动状态"+curThread.isAlive()); System.out.println("测试该线程"+curThreadName+"是否为守护线程"+curThread.isDaemon()); } }
重点介绍sleep()、join()、yield()方法:
sleep()方法是Thread类的静态方法,作用是使当前线程休眠一段时间,传入long类型的参数,单位是毫秒。这种方式会使线程阻塞,阻塞结束线程会变成就绪状态,之后才可能被CPU调度。sleep()方法会持有线程的同步锁休眠,在这段时间内,仍可以控制同步。
join()方法是Thread类的成员方法,由线程对象调用,插队线程。调用该方法的对象代表的线程会插队,直到这个线程结束或插队时间结束,被阻塞的线程才能转为就绪状态继续进行。
yield()方法是Thread的静态方法,不会阻塞线程,使得当前线程从运行状态直接进入就绪状态。
sleep()和wait()的区别:
- 原理不同。sleep()是Thread的静态方法,而wait()是Object类的方法。
- 对锁的处理机制不一样。由于sleep()方法主要是让线程休息一段时间,不涉及线程间的通信,因此调用这个方法不会释放锁。而wait()方法会释放线程占用的锁。所以sleep()容易造成死锁,推荐使用wait()方法。
- 使用区域不同。wait方法必须放在同步控制方法或者同步语句块中使用。而sleep则可在任何地方使用。
- sleep()必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常。在sleep过程中,有可能被其他对象调用它的interrupt(),产生InterrupedException异常。
多线程的同步
上面的售票例子中,多个线程共享一个票的资源,会造成同时买同一张票的问题,这样就需要一定方法控制。Java中提供了一种简单的方式,就是锁的机制,用synchronized关键字实现。
同步方法,直接在需要同步的方法上加上关键字即可,多线程同时操作时,会排队执行这个方法,例如Hashtable实现线程安全就是在方法上加了这个关键字。这种方式虽然简单但效率低。
同步块,在有可能出现同步问题的代码外加上一个代码块synchronized(锁){}。锁可以有三种形式:
1)、类.class,每个类只存在一个Class对象,因此可以用来当作锁。
public class SynchronizedDemo { public static void main(String[] args) { MyThread1 th = new MyThread1(); Thread th1 = new Thread(th); Thread th2 = new Thread(th); th1.start(); th2.start(); } } class MyThread1 implements Runnable { @Override public void run() { method(); } static void method() { System.out.println("静态方法开始"); synchronized(MyThread1.class) { try { System.out.println("静态方法锁定中..."); Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("静态方法锁定结束"); } }
2)、this。指当前调用的对象,成员方法中可以用。
public class SynchronizedDemo { public static void main(String[] args) { MyThread1 th = new MyThread1(); Thread th1 = new Thread(th); Thread th2 = new Thread(th); th1.start(); th2.start(); } } class MyThread1 implements Runnable { @Override public void run() { method(); } void method() { System.out.println("成员方法开始"); synchronized(this) { try { System.out.println("成员方法锁定中..."); Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("成员方法锁定结束"); } }
3)、不变的资源,即成员属性。Java不允许基本数据类型作为锁,可以用自定类的引用当作锁,锁的特点是不可变,因为引用是地址,只要不改变引用地址,锁就可以生效。
public class SynchronizedDemo { public static void main(String[] args) { MyThread1 th = new MyThread1(); Thread th1 = new Thread(th); Thread th2 = new Thread(th); th1.start(); th2.start(); } } class MyThread1 implements Runnable { String str = "aaa"; @Override public void run() { method(); } void method() { System.out.println("成员方法开始"); //用String类型引用做参数 synchronized(str) { try { //这里相当于重新创建一个String对象,改变了引用 str = "123"; System.out.println("成员方法锁定中..."); Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("成员方法锁定结束"); } }
上面的代码,两个线程同时进入了锁定块,说明并没有锁定成功,因此我们在使用成员属性所谓锁时,要保证不变性。
实现售票的同步安全,可以在票卖出前锁定这一段代码。
public class Thread03 { public static void main(String[] args) { TicketsWindow tw = new TicketsWindow(); Thread thread1 = new Thread(tw,"张三"); Thread thread2 = new Thread(tw,"李四"); Thread thread3 = new Thread(tw,"王五"); thread1.start(); thread2.start(); thread3.start(); } } class TicketsWindow implements Runnable{ private int tickets=100; @Override public void run() { while(tickets>0) { synchronized(this) { if(tickets>0) { System.out.println(Thread.currentThread().getName()+"购买第"+tickets--+"张票"); } } //买完休息一会 try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }