线程是指程序在执行过程中,能过执行程序代码的一个执行单元。在Java中,线程有4种状态:运行、就绪、挂起和结束。
进程是指一段正在执行的程序。而线程有时被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)及一些进程级的资源(如打开文件),但是各个线程拥有自己的栈空间。
进程和线程的关系.png
多线程的使用的好处:
- 使用多线程可以减少程序的响应时间。
- 与进程相比,线程的创建和切换开销更小。由于启动一个新的线程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一个进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程少很多。同时多线程在数据共享方面效率非常高
- 多CPU或多核计算机本身就具有执行多线程的能力,在这些计算机上使用多线程能提高CPU的利用率。
- 使用多线程能简化程序的结构,使程序便于理解和维护。
实现多线程的常用方法:
-
继承Thread类,重写run()方法;
Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法是通过Thread类的start()方法。start()方法是一个本地方法,它将启动一个新线程,并执行run()方法(Thread中提供的run()方法是一个空方法)。这种方式通过自定义直接extends Thread,并重写run()方法,就可以起到新线程并执行自己定义的run()方法。调用start()方法后并不是执行多线程代码,而是使用该线程变为可运行态(Runnable),什么时候运行多线程代码由操作系统决定。 -
实现Runnable接口,并实现该接口run()方法;
- 自定义类并实现Runnable接口,实现run()方法。
- 创建Thread对象,用实现Runnable接口的对象作为实例化该Thread对象。
- 调用Thread的start()方法。
class MyThread implements Runnable{
public void run(){
System.out.println("Thread body");
}
}
public class Test{
public static void main(String []args) {
MyThread thread = new MyThread():
Thread t = new Thread(thread);
t.start();
}
}
不管是通过继承Thread类还是通过使用Runnable接口实现多线程的方法,最终还是通过Thread的对象的API来控制线程的。
- 实现Callable接口,重写call()方法
Callable泛型接口实际是属于Executor框架中的功能类,有一个泛型参数V,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能。- Callable可以在任务结束后提供一个返回值(类型为V)的call()函数,Runnable无法提供这个功能。
- Callable中的call()方法可以抛出异常,而Runnable无法提供这个功能。
- 运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程属于异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程会阻塞,直到call()方法结束返回结果。
public class CallableAndFuture{
//创建线程类
public static class CallableTest implements Callable<String> {
public String call() throws Exception{
return "Hello world!"
}
}
public static void main(String []args) {
ExecutorService threaPool = Executor.newSingleThreadExecutor();
//启动线程
Future<String> future = threadPool.submit(new CallableTest());
try{
System.out.println("waiting thread to finish!");
System.out.println(future.get());//等待线程结束,并获取返回结果
}
}
}
注:
一般推荐实现Runnable接口的方式,原因是:
- Thread类定义了多种方法可以被派生类使用或重写。但是只有run()方法是必须被重写的,在run()方法中实现这个线程的主要功能。这当然是实现Runnable接口所需要的方法。
- 很多人认为一个类仅在他们需要被加强或修改时才会被继承,因此,如果没有毕业重写Thread类中的其他方法,那么通过继承Thread的实现方式与实现Runnable接口的效果相同,在这种情况下最好通过实现Runnable接口的方式来创建线程。
run()方法与start()方法有什么区别
系统通过调用线程类的start()方法来启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被JVM来调度执行,在调度过程中,JVM通过调用线程类的run()方法来完成实际的操作做,当run()方法结束后,此线程就会终止。
多线程同步的实现方法
- synchronized关键字
在Java语言中,每个对象都有也给对象锁与之关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段synchronized代码时,需要先获取这个锁,然后去执行相应的代码,执行结束后,释放锁。
synchronized关键字主要有两种用法(synchronized方法和synchronized块),还可以用于静态方法、类或某个实例,但对程序的效率影响很大。- synchronized方法。在方法的声明前加入synchronized关键字。只要把多个线程对类需要被同步的资源的操作放到synchronized方法中,就能保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。
- synchronized块。synchronized块既可以把任意的代码段声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。
synchronized(syncObject){
//访问syncObject的代码
}
- wait()方法和notify()方法
在synchronized代码被执行期间,线程可以调用对象的wait()方法,释放对象锁,进入一个和该对象相关的等待池中,并且可以调用notify()方法或notifyAll()方法通知正在等待的其他线程可以访问。notify()方法仅唤醒一个线程(等待对列中的第一个线程)并允许它去获得锁,notifyAll()方法唤醒所有等待这个对象的线程并允许它们去获得锁(所有线程根据优先级获得锁)。必须放在synchronized块中。 - Lock
- lock()以阻塞的方式获取锁,如果获得锁,立即返回;如果别的线程持有锁,当前线程等待,直到获得锁后返回。
- tryLock()以非阻塞的方式获取锁,只是尝试性地获取一下锁,如果获取到锁,立即返回true;否则,返回false。
- tryLock(long timeout, TimeUnit unit)如果获取了锁,返回true。否则等待参数定的时间单元,在等待的过程中,如果获取锁,返回true;如果超时,返回false。
- lockInterruptibly()如果获取锁,立即返回true,如果没有,当前线程处于休眠状态,直到获得锁。
线程的wait()、sleep()、join()、yeild()
- wait()上述
- sleep()是Tread类的静态方法,作用是使调用线程进入睡眠状态。因为是Thread类的静态方法,因此它不能改变对象的机锁。所以,当一个synchronized块中调用sleep()方法时,线程虽然休眠了,但是对象的机锁并没有被释放,其他线程无法访问这个对象。sleep()方法必须捕抓异常,
- join()等待目标线程执行完成之后 再继续执行
- yeild()线程礼让。目标线程有运行状态转换为就绪状态也就是让出执行权限,让跟其他线程得以优先执行,只会给相同优先级或更高优先级的线程以运行机会。
网友评论