1. 线程与进程
线程是指程序在执行过程中能够执行程序代码的一个执行单元。(线程的四种状态:运行、就绪、挂起和结束)
进程是指一段正在执行的程序,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段和堆空间)以及一些进程级的资源(例如打开的文件),但是各个线程拥有自己的栈空间。
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常会有多个线程互不影响的并发执行。
多线程的作用:
- 使用多线程可以减少程序的响应时间。单线程情况下如果某个操作很耗时会陷入长时间的等待,无法响应其他操作。
- 与进程相比,线程的创建和切换开销更小。由于启动一个新的进程必须给这个进程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
- 多CPU或者多核计算机本身就具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
- 使用多线程能够简化程序的结构。一个非常复杂的进程可以分成多个线程来执行。
2. 同步和异步
- 在多线程中会碰到数据共享的问题。即当多个线程需要访问同一个资源时,他们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用,否则程序的运行结果将会是不可预料的,在这种情况下必须对数据同步。例如多个线程同时对同一数据进行写操作,当线程A需要使用某个资源时,如果这个资源正在被线程B使用,同步机制就会让线程A一直等下去,直到线程B结束对该资源的使用后,线程A才能使用这个资源,由此可见同步机制能够保证资源的安全。
- 要想实现同步操作,必须要获得每一个线程对象的锁。获得它可以保证同一时刻只有一个线程能够进入临界区(访问互斥资源的代码块),并且在这个锁被释放之前,其他线程就不能再进入临界区。如果还有其他线程想要获得该对象的锁,只能进入等待队列等待。只有当拥有该对象锁的线程退出临界区时,锁才被释放,等待队列中优先级最高的线程才能获得该锁,从而进入共享代码区。
- 实现同步:1、利用同步代码块实现同步;2、利用同步方法实现同步
使用synchronized关键字可以来实现同步,它是以很大的系统开销作为代价的,有时候甚至可能造成死锁。 - 异步与非阻塞类似,由于每个线程都包含了运行时自身所需要的数据或方法,因此在进行输入输出处理时,不必关心其他线程的状态或行为,也不必等待输入输出处理完毕才返回。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,异步能提高程序的效率。
3. 实现Java多线程
- 继承Thread类重写run方法
Thread类实现了Runnable接口,它代表一个线程的实例,启动线程的唯一方法就是通过Thread类的Start方法。start方法是一个naive本地方法,它将启动一个新线程并执行run方法。(Thread类中的run方法是一个空方法)。通过自定义类并继承Thread然后重写run方法,就可以启动新线程并执行自己定义的run方法。注意:调用start方法后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行多线程代码由操作系统决定。
class MyThread extends Thread{
public void run() {
System.out.println("MyThread");
}
}
public class Test {
public static void main(String[] args) {
MyThread mt=new MyThread();
mt.start();//开启线程
}
}
- 实现Runnable接口,并实现该接口的run方法
- 自定义类并实现Runnable接口,实现run方法。
- 创建Thread对象,用实现Runnable接口的对象作为参数实例化Thread对象。
- 调用Thread的start方法
class MyThread2 implements Runnable{
public void run() {
System.out.println("MyThread2");
}
}
public class Test {
public static void main(String[] args) {
MyThread mt=new MyThread();
Thread t=new Thread(mt);
t.start();
}
}
一个类是否可以同时继承Thread与实现Runnable接口
可以。
public class Test extends Thread implements Runnable {
public static void main(String[] args) {
Thread t=new Thread(new Test());
t.start();
}
}
因为Test类从Thread类中继承了Run方法,所以可以选择不重写run方法,编译也能通过。
当然也可以重写run()。
4. run()和start()
系统通过调用线程类的start方法来启动一个线程,此时线程处于就绪状态而非运行状态,也就意味着这个线程可以被JVM来调度执行。在调用过程中,JVM通过调用线程类 的run方法来完成实际的操作,当run方法结束后,此线程就会终止。
如果直接调用线程类的run方法,这会被当做一个普通的函数调用,程序中只有主线程这一个线程,即start方法能够异步的调用run方法,但是直接调用run方法却是同步的,无法达到多线程的目的。
class ThreadDemo extends Thread{
public void run() {
System.out.println("ThreadDEMO:BEGIN");
try {
Thread.sleep(1000);
}catch(Exception e) {
e.printStackTrace();
}
System.out.println("ThreadDEMO:end");
}
}
public class ThreadTest {
public static void test1() {
System.out.println("test1:begin");
Thread t1=new ThreadDemo();
t1.start();
System.out.println("test1:end");
}
public static void test2() {
System.out.println("test2:begin");
Thread t1=new ThreadDemo();
t1.run();
System.out.println("test2:end");
}
public static void main(String[] args) {
test1();
try {
Thread.sleep(5000);
}catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println();
test2();
}
}
程序输出:
test1:begin
test1:end
ThreadDEMO:BEGIN
ThreadDEMO:end
test2:begin
ThreadDEMO:BEGIN
ThreadDEMO:end
test2:end
从运行结果可以看出线程t1是在test1方法结束后执行的,不需要等待t1.start()运行结果就可以执行,因此在test1中调用start方法是异步的,所以main线程与t1线程是异步执行的。而test2的运行结果是同步的, System.out.println(“test2:end”);只有等t1.run调用结束后才能执行。
5. 多线程同步的实现方法
1. synchronized关键字
- synchronized方法。
public synchronized void multiThreadAccess()
只要把多个线程对类需要被同步的资源的操作放到 multiThreadAccess()方法中,就能保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高效率引入了synchronized块。 - synchronized块。 synchronized块既可以把任意的代码段声明为 synchronized,也可以指定上锁的对象,有非常高的灵活性。
synchronized(syncObject){
访问syncObject代码。
}
2. wait()方法与notify()方法
- 当使用synchronized来修饰某个共享资源时,如果线程A1在执行synchronized代码,另外一个线程A2也要同时执行同一对象的同一synchronized代码时,线程A2将要等到线程A1执行完成后,才能继续执行。在这种情况下可以使用wait方法和notify方法。
- 在synchronized代码被执行期间,线程可以调用对象的wait方法,释放对象锁,进入等待状态,并且可以调用notify方法和notifyALL方法通知正在等待的其他线程。notify方法仅唤醒一个线程(等待队列中的第一个线程)并允许它去获得锁,notifyALL方法唤醒所有等待这个对象的线程并允许他们去获得锁(并不是让所有唤醒线程都获取到锁,而是让他们去竞争)。
3. Lock
6. Sleep()与Wait()区别
- 原理不同。sleep方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等待计时时间一到,此线程就会苏醒。而wait方法是Object类的方法,用于线程间的通信,这个方法会让当前拥有该对象锁的进程等待,直到其他线程调用notify方法时才会醒来。不过开发人员可以设置一个时间让他自动醒来。与wait方法配套的还有notify()和notifyAll()。
- 对锁的处理机制不同。sleep()的作用主要是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用sleep方法并不会释放锁。而调用wait()后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
- 使用区域不同。由于wait方法的特殊意义,因此它必须放在同步控制方法或者同步语句块中使用,而sleep方法则可以放在任何地方使用。
- sleep()必须捕获异常而wait()notify()和notifyAll()不需要。在sleep的过程中有可能被其他对象调用它的interrupt(),产生InterruptedException。
- 由于sleep不会释放锁标志容易导致死锁问题的发生,因此一般情况下不推荐使用sleep方法而使用wait方法。
sleep()与yield()的区别
- sleep()给其他线程运行机会时不考虑线程的优先级,而yield()只会给相同优先级或更高优先级的线程以运行的机会。
- 线程执行sleep方法后会转入阻塞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行,而yield()只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入到可执行状态后马上又被执行。
- sleep()抛出InterruptedException,而yield()没有声明任何异常。
- sleep()比yield()具有更好的可移植性。
sleep()方法指定的时间为线程不会运行的最短时间。当睡眠时间结束后,线程会返回到可运行的状态,不是运行状态,还需要等待cpu调度执行。因此sleep方法不能保证该线程睡眠到期后就开始执行。
7. 终止线程
- Thread.stop终止线程时,它会释放已经锁定的所有监视资源。如果当前任何一个受这些监视资源保护的对象处于不一致的状态,其他线程将会看到这个不一致的状态,这可能会导致程序执行的不确定性。
- 调用suspend()容易死锁(死锁指的是两个或两个以上的进程在执行的过程中,因相互争夺资源而造成一种相互等待的现象,如果无外力作用,它们都将无法推进。)suspend()不会释放锁,如果一个suspend挂起一个有锁的线程,那么在锁恢复之前将不会被释放。当另一个线程试图去获得互斥资源的锁时程序就会发生死锁。鉴于这两种方法的不安全性,不推荐使用。
- 一般是让线程自行结束进入dead状态。一个线程进入dead状态即执行完run方法。即如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run方法的执行。
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
public void run() {
System.out.println("thread go to sleep");
try {
Thread.sleep(1000);//用休眠来模拟线程被阻塞
System.out.println("thread finish");
}catch(InterruptedException e) {
System.out.println("thread is interupted");
}
}
});
t.start();
t.interrupt();
}
程序输出:
thread go to sleep
thread is interupted
join()方法的作用是让调用该方法的线程在执行完run()方法后,再执行join方法后的代码。可以通过线程A的join()方法等待线程A的结束,或者使用A的join(2000)方法来等待线程A的结束,但最多只等2s。