Java多线程--基础概念与操作

一、多线程相关概念

1.1 什么是多线程

        在早期的计算机中时没有操作系统的,计算机开启后只能执行一个程序,直到结束。操作系统的出现使得计算机可以同时执行多个程序,操作系统为每个程序分配不同的进程,每个进程拥有独立的句柄、资源等,使得计算机可以同时执行多个程序。但是进程的创建和销毁耗费的代价太大,因此衍生出线程的概念。允许在一个进程中创建多个线程,这些线程共享进程的资源,并且每个线程拥有自己独立的程序计数器、线程局部变量等资源。线程也被称为进程的轻量型运动实体。

1.2 线程的优势

线程的出现带来很多的好处:

1、发挥多核处理器的性能:在多喝处理器上执行单线程任务是对多核的浪费,因为总有核心在空闲着,多线程的出现能充分发挥多核的优势。

2、化整为零:往往在一个复杂的应用中包含许多不同类型的任务,将这些不同类型的任务分配给不同的线程去执行会比将其混在同一个线程中去执行要好,因为每个线程更加的简单清晰,更容易测试等。

3、异步事件处理:当一个线程处理的任务遇到阻塞时如IO阻塞,cpu可以调度其他线程去执行而不是在那傻傻的等到IO结束在执行其他任务。

4、更好的用户体验:当多个用户像你的服务发送请求时,你一个线程依次执行任务会使得排在后面的用户等待时间过长得不到响应,带来不好的体验。但使用多个线程可以让每个用户都能很快的得到响应(尽管这不能高执行速度),用户会觉得自己的请求正在被处理,获得更好的体验。

1.3 多线程引入的问题

多线程虽然带来了许多优势,但是不当使用多线程也会引入许多问题。

1、安全性:多线程同时访问操作共享资源会造成不可预料的结果。

2、活跃性问题:由于多线程争夺资源导致无限循环,出现死锁、活锁、线程饥饿等问题。

3、性能问题:多线程的引入是为了提高处理器的性能,但是滥用线程不仅不能提高性能反而会导致性能下降。如创建过多的线程使CPU频繁地去执行线程切换导致性能降低。

二、Java中创建线程

在java中创建线程的方式有三种:继承Thread类重写run方法、实现runnable接口重写run方法和实现callable接口重写call方法配合futureTask使用

2.1、继承Thread类

public class MyThread extends Thread{
	
	public static void main(String[] args) {
		MyThread myThread = new MyThread();
		myThread.start();
	}
	
	@Override
	public void run() {
		int i = 0;
		for(;i<100;i++) {
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

}

继承Thread类的方式创建线程算是最简单的了,但是你的线程类往往要继承项目中的其他类,而Java是单继承机制的,所以使用此方法会有很大的局限性。

2.2、实现runnable接口

public class MyRunnable implements Runnable {

	public static void main(String[] args) {
		MyRunnable myRunnable = new MyRunnable();
		Thread thread = new Thread(myRunnable);
		thread.start();
	}

	@Override
	public void run() {
		int i = 0;
		for (; i < 100; i++) {
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

}
Thread类的构造方法允许传入一个实现runnable接口的target进去,线程启动将会执行target.run方法。实现runnable接口的方式可以很好的避免单继承问题。

2.3、实现callable接口

	public static void main(String[] args) throws Exception {
		System.out.println("12123");
		FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
		new Thread(futureTask).start();
		// futureTask可以在指定时间内获取线程执行的返回值,超时则丢弃任务
		//  因此futureTask可以用作异步任务处理
		futureTask.get(1000, TimeUnit.SECONDS);
	}

	@Override
	public Integer call() throws Exception {
		int i = 0;
		for(;i<100;i++) {
			try {
				Thread.sleep(1000);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return i;
	}
call方法与run方法最大的区别在于call方法存在返回值futureTask的get方法可以获取这个返回值。使用此种方法实现线程的好处是当你创建的任务的结果不是立即就要时,你可以提交一个线程在后台执行,而你的程序仍可以正常运行下去,在需要执行结果时使用futureTask去获取即可。这是一种典型的异步任务处理的方法。


三、线程的各种状态及其转换

一个线程的生命周期包含以下五种状态:

1、初始状态:线程被new出来时处于此种状态

2、就绪状态:当一个线程的start方法被调用或正在运行的线程CPU时间片耗尽或处于阻塞状态的线程满足了运行条件时处于此种状态。就绪状态的线程处于CPU调度队列中等待获得CPU时间片执行。

3、运行状态:处于就绪状态的线程获取到CPU时间片,正在执行。

4、阻塞状态:当线程要执行的必要条件无法满足时处于此种状态,如等待其他线程释放锁、等待IO等。当这些条件得到满足后线程变为就绪状态。

5、死亡:当线程run方法执行完或者遇到异常退出时线程死亡,线程的生命周期就此终止。

以下为线程状态转换图:


四、常用方法总结

4.1、wait、notify和notifyAll

这三个方法并不属于Thread类而是属于Object类,这意味着在Java总所有的类都拥有这三个方法。

在Java中每个对象都可以用作实现一个同步的锁,这些锁被称为内置锁或者监视器锁。当一个线程调用一个对象的wait方法时,他会让自己(即当前线程)陷入等待状态,并放弃所持有的锁,当其他线程再调用这个对象的notify或notify方法时会唤醒这个对象内置锁上等待的线程,此时刚刚陷入等待的线程可以在停下来的地方继续向下执行。

wait方法:使调用此方法的线程立即进入阻塞状态并放弃所持有的锁

notify与notifyAll:使当前线程放弃持有的某个对象上的锁并唤醒在这个对象锁上等待的其他线程,notify唤醒单个线程,notifyAll唤醒全部线程。调用notify方法后当前线程会在执行完剩余代码后放弃该对象的锁而不是像wait方法那样立即释放。

wait和notify方法可以实现线程之间的通信,典型的使用就是用来实现生产者和消费者问题--当队列满了生产者阻塞并唤醒消费者,当队列空了消费者阻塞并唤醒生产者。

如下为使用wait与notify实现的生产者消费者问题:

public class Test {
	public static void main(String[] args) {
		Queue<Integer> queue = new LinkedBlockingQueue<>();
		new Producer(queue).start();
		new Customer(queue).start();
	}
}

class Producer extends Thread {
	Queue<Integer> queue;

	public Producer(Queue<Integer> queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (queue) {
				try{
                    Thread.sleep(500);
                } catch (Exception e) {}
				if (queue.size() >= 3) {
					try {
						System.out.println("队列已满,生产者阻塞");
						queue.wait();
					} catch (InterruptedException e) {
					}
				}
				// 唤醒消费者
				queue.notify();
				int i = new Random().nextInt(100);
				System.out.println("生产" + i);
				queue.add(i);
			}
		}
	}

}

class Customer extends Thread {
	Queue<Integer> queue;

	public Customer(Queue<Integer> queue) {
		this.queue = queue;
	}

	@Override
	public void run() {
		while (true) {
			synchronized (queue) {
				try{
                    Thread.sleep(500);
                } catch (Exception e) {}
				if (queue.isEmpty()) {
					try {
						System.out.println("队列已空,消费者阻塞");
						queue.wait();
					} catch (InterruptedException e) {
					}
				}
				// 唤醒生产者
				queue.notify();
				int i = queue.poll();
				System.out.println("消费:" + i);
			}
		}
	}
}

运行结果如下:



4.2、join()方法

join方法为Thread类所有方法,当在一个线程A中调用另一个线程B的join方法时,线程A会等待线程B执行完再进行执行。

4.3、yield()方法

yield方法会发送一个通知给调度器表示当前线程愿意让出CPU时间片给其他线程先执行,但是这种让步策略并不能得到保障,因为调度器可以选择忽视这个通知,这取决于当前下同的线程调度策略。

4.4、sleep(long millis)方法

sleep方法会使当前线程休眠指定时间,处于休眠的线程并不会放弃已经持有锁,放不放弃锁是sleep方法与wait方法的最大区别。

4.5、setPriority(int newPriority)

setPriority可以设置线程的优先级,传入一个1到10之间的整数,数值越大表示优先级越高,在一个母线程中创建一个子线程则子线程的优先级默认与母线程相同

java中设置线程优先级有10个等级可选,但是这种等级制度在不同的系统中不能得到很好的映射,某些系统可能没有10个线程等级,某些则超过10个等级,Java中默认给我们提供了三种可选等级,大部分时候使用这三种等级即可:

    /**
     * The minimum priority that a thread can have.
     */
    public final static int MIN_PRIORITY = 1;


   /**
     * The default priority that is assigned to a thread.
     */
    public final static int NORM_PRIORITY = 5;


    /**
     * The maximum priority that a thread can have.
     */
    public final static int MAX_PRIORITY = 10;

4.6、stop()和suspend()

这两个方法是被废弃的方法。

stop():立即停止一个线程的运行并放弃他所持有的所有的锁。

为何弃用:这个立即停止是强制性的,你甚至可以在线程还没运行时就调用     stop使其强制停下,这样线程在运行伊始就会被停止。显然这种强制的停止一个线程并释放锁的方式是不安全的,因为当这个线程在处理时会使所持有锁的对象处于中间状态,这种状态在系统中不应该存在,如果线程能正常执行或异常退出时,这种中间状态是不会被其他线程看到的。但是调用stop方法会使当前线程强制性的释放这个对象的锁,使其在中间状态时就对其他线程可见,这显然会导致线程安全问题。

stop被弃用后的替代:stop不能使用时如何停止一个线程呢?官方推荐的方式是在线程运行时添加一个状态变量,线程会不断的检查这个变量来决定是否停下,如下:

	@Override
	public void run() {
		
		while(!isStop) {
			doSomething();
		}
}

run方法一旦发现isStop为true会理解结束,这也是最好的停止线程的方法了。但是这种方式有个弊端:一旦doSomething方法执行时间过长或者在等待时会导致当前线程无暇去检测isStop变量,此时建议使用interrupt()方法去中断这个线程。

suspend():暂停一个线程但是不放弃所持有的锁。

suspend被弃用原因:suspend被调用后当前线程会暂停下来但是不会放弃持有的对象锁,直到其他的线程调用resume方法将此线程唤醒。如果唤醒线程需要拿到被暂停的线程所控制的锁才能运行时会出现死锁状态,即唤醒线程不拿到锁则无法唤醒暂停线程,而暂停线程不被唤醒则无法释放锁。因此suspend被弃用,他的兄弟resume方法自然也被弃用。

suspend与wait和sleep的区别:wait会使当前线程等待但是会释放锁,sleep不释放锁但是会自己唤醒自己,因此sleep与wait都不会死锁。而suspend即不释放锁也不能自己唤醒,会产生死锁所以被弃用。


4.7、interrupt()、interrupted()和isInterrupted()

理论上来讲一个线程不能被别的线程中断或终止,只能由自己中断和终止,这也是stop方法被弃用的原因。

interrupt:此方法会向此线程发送一个信号:你被中断了,并将该线程的中断标志改为true。还记得stop方法被废弃的替代方案吗,使用一个标志位来决定是否停下线程,interrupt方法实际上也是这样操作的。该线程正常处理时会不断的去读取这个中断标志,发现为true时则停止,当该线程处于阻塞状态时(如调用了wait、sleep、join)会让该线程立即抛出一个InterruptedException并且清除该线程的中断状态,这样可以直接进入该线程的catch代码块中进行执行,在catch中可以对此线程进行特殊处理(立即结束或者做一些处理后再次运行)

interrupted:此方法会执行两步操作:1、返回该线程的中断标志。2、将该线程的中断标志设置为false。显然这两部操作是要满足原子性的,此方法也是被synchronized修饰过的。有些线程在被调用了interrupt方法后并不想停下,可以在他的catch代码块中调用自己的interrupted方法,让自己继续正常运行。

isInterrupted此方法会返回该线程的中断标志















展开阅读全文

没有更多推荐了,返回首页