Andy Back-end Dev Engineer

Java基础-多线程

2018-10-06

多线程概念

程序引入:

public class ThreadDemo {
	public static void main(String[] args) {
		// 代码1
		show();
		// 代码2
	}

	private static void show() {
		// 代码11
		method1();
		method2();
		// 代码22
	}

	private static void method2() {
		// 代码111
	}

	private static void method1() {
		// 代码222
	}
}

上面一段代码的程序执行流程如下: 单线程程序流程 可以看出这是一段单线程程序的代码 多线程程序的执行流程举例: 多线程执行流程 如果程序只有一条执行路劲,那么就是单线程程序,如果程序有多条执行路劲,那么就是多线程程序

什么是进程

进程是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。一个进程对应一个应用程序。在java的开发环境下启动JVM,就表示启动了一个进程。现代的计算机都是支持多进程的,在同一个操作系统中,可以同时启动多个进程。

多进程的作用

单进程计算机只能做一件事情。但需要电脑同时开多个程序时就需要用到多进程。

一个问题:对于单核计算机来讲,在同一个时间点上,游戏进程和音乐进程是同时在运行吗? 不是。因为计算机的CPU只能在某个时间点上做一件事。由于计算机将在“游戏进程”和“音乐进程”之间频繁的切换执行,切换速度极高,人类感觉游戏和音乐在同时进行。

注意:

  1. 多进程的作用不是提高执行速度,而是提高CPU的使用率。
  2. 进程和进程之间的内存是独立的。

什么是线程

线程是一个进程中的执行场景。一个进程可以启动多个线程。 线程是程序的执行单元,执行路径,是程序使用CPU的最 注意:同一个进程中的线程共享其进程中的内存和资源(共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的。)

多线程的作用

  1. 多线程不是为了提高执行速度,而是提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU的执行权,多个进程在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权,而且不能保证在哪个时刻能抢到,所以线程的执行有随机性。
  2. 线程和线程共享“堆内存和方法区内存”,栈内存是独立的,一个线程一个栈。
  3. 可以给现实世界中的人类一种错觉:感觉多个线程在同时并发执行。

java程序的运行原理

java命令会启动java虚拟机,启动JVM,等于启动了一个应用程序,表示启动了一个进程。该进程会自动启动一个“主线程”,然后主线程去调用某个类的main 方法。所以main方法运行在主线程中。

注意:JVM的启动时多线程的,因为垃圾回收线程也会先启动,防止出现内存溢出。

线程的生命周期

生命周期

新建(New)

创建但为启动

可运行(Runnable)

一旦调用start()方法线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,取决于操作系统给线程提供运行的时间。(Java规范没有将他作为一个单独的状态)包括了操作系统线程状态中的running和ready。

阻塞(Blocking)

等待获取一个排它锁,如果其线程释放了锁就会结束此状态。

无限期等待(Waiting)

限期等待(Timed Waiting)

死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束。

线程的调度

线程有两种调度模型:

  1. 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
  2. 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。

Java使用的是抢占式调度模型。 可以通过以下方法进行设置和查询优先级

public final int getPriority()
public final void setPriority(int newPriority)

优先级字段 public static final int MAX_PRIORITY 10 public static final int MIN_PRIORITY 1 public static final int NORM_PRIORITY 5

注意:线程优先级仅表示获取CPU时间片的几率高,但是再运行此时多的时候才能看到较好的效果。

使用线程

继承Thread类

Thread类实现了Runable接口,自定义一个Thread的子类并实现run()方法。

public class MyThread extends Thread{
	// 重写run()方法,用来包含被线程执行的代码
	@Override
	public void run() {
		// 线程执行的代码一般时比较耗时的,这里进行模拟
		for(int i=0; i<100; i++) {
			System.out.println(i);
		}
	}
}

测试:

public class MyTest {
	public static void main(String[] args) {
		// 创建对象
		MyThread thread1 = new MyThread();
		MyThread thread2 = new MyThread();
		// 这里使用run方法相当于普通的方法调用,得到的时单线程的效果
		// thread.run();
		// thread.run();
		// 先启动线程,然后再由jvm调用,多线程的效果
		//thread.start();
		// IllegalThreadStateException
		// 因为这么做相当于thread线程被调用了两次,而不是两个线程启动
		//thread.start();
		
		// 需要创建两个线程
		thread1.start();
		thread2.start();
	}
}

注意: run()仅仅时封装被线程执行的代码,直接调用时普通方法,单线程的效果。 start()时先启动了线程,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被JVM调度时会执行该线程的 run() 方法,多线程的效果。

实现 Runnable 接口

实现 Runnable接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread来调用。可以说任务是通过线程驱动从而执行的。

public class MyRunnable implements Runnable{
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + ": hello");
	}
}
public class Test {
	public static void main(String[] args) {
		MyRunnable runnable = new MyRunnable();
		Thread thread = new Thread(runnable);
		thread.start();
	}
}

两种方法比较

  1. 可以避免由于Java单继承带来的局限性。
  2. 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

线程控制

Sleep

public static void sleep(long millis)方法会休眠当前正在执行的线程,millisec 单位为毫秒. sleep() 可能会抛出InterruptedException,因为异常不能跨线程传播回main()中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。

// 休眠一秒
try {
	Thread.sleep(1000);
	} catch (InterruptedException e) {
	e.printStackTrace();
    }

Join

线程加入,在线程中调用另一个线程的join()方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 对于以下代码,虽然b线程先启动,但是因为在 b 线程中调用了a线程的join()方法,b线程会等待a线程结束才继续执行,因此最后能够保证a线程的输出先于b线程的输出。

public class JoinExample {
	
	private class A extends Thread {

		@Override
		public void run() {
			System.out.println("A");
		}
	}
	
	private class B extends Thread {
		private A a;
		
		B(A a) {
			this.a = a;
		}
		
		@Override
		public void run() {
			try {
				a.join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("B");
		}
	}
	
	public void test() {
		A a = new A();
		B b = new B(a);
		b.start();
		a.start();
	}
}
public class MyTest {
	public static void main(String[] args) {
		JoinExample example = new JoinExample();
		example.test();  // A B
	}
}

yield

线程礼让,对静态方法Thread.yield()的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,该方法不会阻塞线程,只会将线程转换成就绪状态,让系统的调度器重新调度一次,而且也只是建议具有相同优先级的或者更高级别的其它线程可以运行。

public void run() {
    Thread.yield();
}

Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。 使用setDaemon()方法将一个线程设置为守护线程。

中断

InterruptedException

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

public class InterruptedDemo {
	public static void main(String[] args) {
		Thread t1 = new MyThread();
		
		t1.start();
		t1.interrupt();
		
		System.out.println("main run");
	}
}

public class MyThread extends Thread{
	@Override
	public void run() {
		try {
			Thread.sleep(2000);
			System.out.println("Thread run");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

运行结果
main run
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at love.minmin.thread11.MyThread.run(MyThread.java:7)

interrupted()

如果一个线程的run()方法执行一个无限循环,并且没有执行 sleep()等会抛出InterruptedException 的操作,那么调用线程的interrupt()方法就无法使线程提前结束。

但是调用 interrupt()方法会设置线程的中断标记,此时调用 interrupted()方法会返回true。因此可以在循环体中使用interrupted()方法来判断线程是否处于中断状态,从而提前结束线程。

public class InterruptedDemo {
	public static void main(String[] args) {
		MyThread2 t1 = new MyThread2();
		
		t1.start();
		t1.interrupt();
		
		System.out.println("main run");
	}
}

public class MyThread2 extends Thread{
	@Override
	public void run() {
		while(!interrupted()) {
			//..
		}
		System.out.println("Thread end");
	}
}

运行结果
main run
Thread end

互斥同步

先看一个简单多线程实现卖票功能的程序

public class SellTicket implements Runnable{
	private int ticket = 100;
	
	@Override
	public void run() {
		while(ticket > 0) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			// 这里有可能出现t1,t1,t3输出相同的票
			// 因为cpu的操作是原子性的
			// 加入t1抢到执行权,这个时候先记录ticket以前的值,然后--,然后输出
			// 输出之后t2抢到,这个时候t2输出就是--之后的值,但是如果在之前抢到,那么t2输出也是100
			System.out.println(Thread.currentThread().getName() + ": " + ticket--);
			// 也会出现负数票,是随机性和延迟导致的,最多输出-1张票
		}
	}
}
public class Test {
	public static void main(String[] args) {
		SellTicket ticket = new SellTicket();
		
		Thread thread1 = new Thread(ticket);
		Thread thread2 = new Thread(ticket);
		Thread thread3 = new Thread(ticket);
		
		thread1.start();
		thread2.start();
		thread3.start();
	}
}

该程序运行时存在一张票卖多次以及负数票的情况,需要进行加锁 Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是JVM实现的synchronized,而另一个是 JDK 实现的 ReentrantLock。

synchronized

同步代码块

    private Object obj = new Object();
    
	public void run() {
		while(ticket > 0) {
			synchronized (obj) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + ": " + ticket--);
			}
		}
	}

这里同步对象可以选择任意一个对象,但是要保证三个线程所使用的对象为同一个

同步一个方法

public void run() {
		while(ticket > 0) {
			if (ticket % 2 ==0) {
				synchronized (this) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);
				} 
			} else {
				ticket();
			}
		}
	}

private synchronized void ticket() {
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + ": " + ticket--);
	}

同步方法的锁对象为this

同步一个静态方法

@Override
public void run() {
	while(ticket > 0) {
		if (ticket % 2 ==0) {
			synchronized (SellTicket.class) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);				} 
		} else {
			ticket();
		}
	}
}

private static synchronized void ticket() {
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + ": " + ticket--);
	}

静态方法锁对象为类的字节码文件

ReentrantLock

JDK1.5ReentrantLock是java.util.concurrent(J.U.C)包中的锁。 代码举例:

public class SellTicket implements Runnable{
	private int ticket = 100;
	private Lock lock = new ReentrantLock();

	@Override
	public void run() {
		while(true) {
			lock.lock();
			
			try {
				if (ticket > 0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + ": " + ticket--);
				} 
			} finally {
			    // 必须要解锁,防止死锁问题
				lock.unlock();
			}
		}
		
	}
}

死锁

死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:

  • 互斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 deadlock

代码举例

public class DeadLock extends Thread{
	private boolean flag;
	
	public DeadLock(boolean flag) {
		this.flag = flag;
	}
	
	@Override
	public void run() {
		if(flag) {
			synchronized (MyLock.objA) {
				System.out.println("if objA");
				synchronized (MyLock.objB) {
					System.out.println("if objB");
				}
			}
		} else {
			synchronized (MyLock.objB) {
				System.out.println("else objB");
				synchronized (MyLock.objA) {
					System.out.println("else objA");
				}
			}
		}
	}
}

class MyLock {
	public static final Object objA = new Object();
	public static final Object objB = new Object();
}

使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

线程间通信问题

线程间通信问题指的是线程间争对同一种资源的操作。 注意: 1.不同种类的线程都要加锁 2.锁要是同一个

wait() notify() notifyAll()

它们都属于Object的一部分,而不属于Thread。因为这些方法只能被锁对象调用,而锁对象是任意对象,所以是Object。 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用notify()或者notifyAll() 来唤醒挂起的线程。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者notifyAll()来唤醒挂起的线程,造成死锁。

notify()唤醒之后不代表你可以立马执行,必须还要抢CPU执行权

wait()和sleep()的区别 1.wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; 2.wait() 会释放锁,sleep() 不会。 3.sleep()必须指定时间,wait()不需要


下一篇 Python-简介

Comments

Content