Java之路(五): 线程

线程

并行和并发

并行: 指两个或多个事件在同一时刻发生;
并发: 指两个或读个事件在同一时间段内发生。

Concurrency is when two tasks can start, run, and complete in overlapping time periods. Parallelism is when tasks literally run at the same time, eg. on a multi-core processor.
Concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations.
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.
An application can be concurrent – but not parallel, which means that it processes more than one task at the same time, but no two tasks are executing at same time instant.
An application can be parallel – but not concurrent, which means that it processes multiple sub-tasks of a task in multi-core CPU at same time.
An application can be neither parallel – nor concurrent, which means that it processes all tasks one at a time, sequentially.
An application can be both parallel – and concurrent, which means that it processes multiple tasks concurrently in multi-core CPU at same time.
Vipin Jain. Differences between concurrency vs. parallelism

翻译成中文:

并发是两个任务可以在重叠的时间段内启动,运行和完成。并行是任务在同一时间运行,例如,在多核处理器上。
并发是独立执行过程的组合,而并行是同时执行(可能相关的)计算。
并发是一次处理很多事情,并行是同时做很多事情。
应用程序可以是并发的,但不是并行的,这意味着它可以同时处理多个任务,但是没有两个任务在同一时刻执行。
应用程序可以是并行的,但不是并发的,这意味着它同时处理多核CPU中的任务的多个子任务。
一个应用程序可以即不是并行的,也不是并发的,这意味着它一次一个地处理所有任务。
应用程序可以即是并行的也是并发的,这意味着它同时在多核CPU中同时处理多个任务。


进程与线程

进程: 有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个是线程。
线程: 堆空间是共享的,栈空间是独立的,线程消耗的资源也比进程小,相互之间是可以影响的,又称为轻型进程或进程元

Java程序的进程(Java的一个程序运行在系统中)里至少包含主线程和垃圾回收线程(后台线程)。

区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  1. 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
  2. 线程的划分尺度小于进程,使得多线程程序的并发性高。
  3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。

线程调度:

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令。所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务。
  在运行池中,会有多个处于就绪状态的线程在等待CPU,JVM的一项任务就是负责线程的调度,JVM采用的是抢占式调度,没有采用分时调度,因此可能会造成多线程执行结果的随机性。

多线程的优势

1.进程之间不能共享内存,而线程之间共享内存(堆内存);
2.系统创建进程时需要为该进程重新分配系统资源,创建线程则代价较小,因此实现多任务并发时,多线程效率更高;
3.Java语言本身内置多线程功能的支持,而不是单纯的作为底层系统的操作方式,从而简化了多线程编程;

Java操作进程(IO流)

1.Runtime类的exec方法

public static void main(String[] args) throws IOException {
	//方式一:使用Runtime类的exec方法
	Runtime runtime = Runtime.getRuntime();
	runtime.exec("notepad");	
}

2.ProcessBuilder类中的start方法:

public static void main(String[] args) throws IOException {
	//方式二:使用ProcessBuilder中的start方法
	ProcessBuilder pb = new ProcessBuilder("notepad");
	pb.start();
}

创建并启动线程

传统的有两种方式:

方式一:继承Thread类:
1.定义一个类A,继承与java.lang.Thread类;
2.在A类中覆盖Thread类中的run方法;
3.我们在run方法中编写需要执行的操作---->线程执行体;
4.在main方法(线程)中,创建线程对象,并启动线程;

创建线程类:                  A类 a = new A类();
调用线程对象的start方法:      a.start();         //启动一个线程

注意:千万不要调用run方法,如果调用run方法,就好比对象调用方法,依然只有一个线程,并没有开始新的线程。start方法会在底层调用run方法。

//播放音乐线程类
class MusicThread extends java.lang.Thread {

	public void run() {
		for (int i = 1; i <= 50; ++i) {
			System.out.println("播放音乐" + i);
		}
	}
}

// 方式一:继承Thread类
public class ExtendsThreadDemo {

	public static void main(String[] args) {
		// 主线程:运行游戏
		for (int i = 1; i <= 50; ++i) {
			System.out.println("打游戏"+ i);
			if (i == 10){
				//创建线程对象,启动线程
				MusicThread t = new MusicThread();
				t.start();
			}
		}
	}
}

注意:线程只能启动一次,其结果有不可预知性

方式二:实现Runnable接口:
1.定义一个类A实现于java.lang.Runnable接口,注意A类不是线程类
2.在A类中覆盖Runnable接口中的run方法
3.我们在run方法中编写需要执行的操作---->run方法里的,线程执行体
4.在main方法中,创建线程对象,并启动线程。

创建线程类对象:
Thread t = new Thread(new A());
调用线程方法的start方法:
t.start();
//播放音乐的类
class MusicRunnableImpl implements java.lang.Runnable {
	public void run() {
		for (int i = 1; i <= 50; ++i) {
			System.out.println("播放音乐" + i);
		}
	}
}

public class ImplementsRunnableDemo {
	public static void main(String[] args) {
		// 主线程:运行游戏
		for (int i = 1; i <= 50; ++i) {
			System.out.println("打游戏" + i);
			if (i == 10) {
				// 创建线程对象,启动线程
				Runnable target = new MusicRunnableImpl();
				Thread t = new Thread(target);
				t.start();
			}
		}
	}
}

还可以使用匿名内部类实现:

1.接口

public static void test1() {
	// 主线程:运行游戏
	for (int i = 1; i <= 50; ++i) {
		System.out.println("打游戏" + i);
		if (i == 10) {
			// 创建线程对象,启动线程
			new Thread(new Runnable() {
				public void run() {
					for (int i = 1; i <= 50; ++i) {
						System.out.println("播放电影" + i);
					}
				}
			}).start();
		}
	}
}

2.类

public static void main(String[] args) {
	// 主线程:运行游戏
	for (int i = 1; i <= 50; ++i) {
		System.out.println("打游戏" + i);
		if (i == 10) {
			// 创建线程对象,启动线程
			new Thread() {
				public void run() {
					for (int i = 1; i <= 50; ++i) {
						System.out.println("播放电影" + i);
					}
				}
			}.start();
		}
	}
}

继承方式和实现方式的区别

继承方式:
1.Java中类是单继承的,如果继承了Thread类,该类就不能有其他的直接父类;
2.从操作上分析,继承方式更简单,获取线程名字更简单(getName()),操作上更简单;
3.从多线程共享同一个资源方面来说,继承方式无法实现使用共享资源
实现方式:
1.Java中类可以实现多接口,此时该类还可以继承其他类,还可以实现其他接口(设计上更优雅);
2.从操作上分析,稍微复杂点,获取线程名字相对麻烦一点,需要使用Thread.currentThread()来获取当前线程对象的引用;
3.从多线程共享同一个资源上分析,实现方式可以做到共享资源

线程安全问题

当多线程并发访问同一个资源对象的时候可能出现线程不安全的问题

我们使用Thread.sleep()来暂停当前线程一定的时间,让其他线程运行去抢资源,用于模拟网络延迟。

结论: 在线程的run方法上不能使用throws来声明抛出异常,只能在方法中使用try-catch来处理异常。
原因是: 子类覆盖父类的方法的原则,子类不能抛出新的异常,在Runnable接口中的run方法没有声明抛出异常

值得注意的是,并不是使用了sleep方法之后才出现了问题,只是让问题暴露的更明显。

for (int i = 1; i <= 50; ++i) {
	if (num > 0) {
		//模拟网络延迟
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");
	}
}

应将System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");同时操作(原子操作,不能分割,必须保证同步进行)。

要解决上述读线程并发访问同一资源的安全性问题:

线程同步

方式一: 同步代码块
语法:

synchronized(同步锁)
{
	需要操作的代码
}

同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。
也叫同步监听对象/同步锁/同步监听器/互斥锁
对象的同步锁只是一个概念,可以想象在对象上标记了一个锁。
Java程序运行使用任何对象作为同步监听对象,但是一般的,我们试验当前并发访问的共同资源作为同步监听对象。
注意: 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他线程在外面进行等待。

public void run() {
	for (int i = 1; i <= 50; ++i) {
		synchronized (this) {//该对象表示多线程共享的资源
			if (num > 0) {
				// 模拟网络延迟
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + 
				                        "吃了编号为" + num-- + "的苹果");
			}
		}
	}
}

方式二: 同步方法
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

synchronized public void run() {
	//TODO
}

同步锁是什么:
对于非static方法,同步锁就是this;
对于static方法,我们使用当前方法所在类的字节码对象(XXX.class)。

注意: 不要使用synchronized修饰run方法,修饰之后某一个线程就执行完全部的功能,好比是多个线程出现串行。
解决方案: 把需要同步的代码定义在新的方法中,并将该方法用synchronized修饰,再在run方法中调用该方法。

class Apple2 implements Runnable {
	private int num = 50;// 苹果总数

	public void run() {
		for (int i = 1; i <= 50; ++i) {
			eat();
		}
	}

	synchronized private void eat() {
		if (num > 0) {
			// 模拟网络延迟
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + "吃了编号为" + num-- + "的苹果");
		}
	}
}

synchronized的优缺点:
优点: 保证了多线程并发访问时的同步操作,避免了线程的安全性问题;
缺点: 使用synchronized方法或代码块性能会降低。

面试题:
1.StringBuilder和StringBuffer的区别;
2.ArrayList和Vector(安全但是性能低)的区别;
3.HashMap和HashTable(安全但是性能低)的区别;
建议: 尽量减小synchronized的作用域。


单例模式-懒加载模式:

public class ArrayUtil2 {
	private ArrayUtil2() {
	}

	private static ArrayUtil2 instance = null;
	public static ArrayUtil2 getInstance() {
		if (instance == null) {
			instance = new ArrayUtil2();
		}
		return instance;
	}

	public void sort(int[] arr) {

	}

但是上述代码会存在线程不安全的问题,我们使用synchronized:

public class ArrayUtil2 {
	private ArrayUtil2() {
	}

	private static ArrayUtil2 instance = null;

	// 同步方法
	synchronized public static ArrayUtil2 getInstance() {
		if (instance == null) {
			instance = new ArrayUtil2();
		}
		return instance;
	}

	public void sort(int[] arr) {

	}

但是synchronized作用域太大了,损耗性能,我们减少synchronized的作用域:
解决方案:使用双重检查加锁机制

双重检查加锁:
可以既实现线程安全,又能够使性能不受很大影响
所谓的双重检查加锁机制指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块,这是第一重检查。进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来就只需要同步一次,从而减少多次在同步情况下进行判断所浪费的时间。

“双重检查加锁”机制的实现使用volatile关键字,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能够正确处理该变量。
注意: 在Java1.4以及之前版本中,很多JVM对于volatile的实现问题,会导致“双重检查加锁”的失败,因此双重检查加锁机制只能使用于Java5及以上的版本。
提示: 由于 volatile 关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此建议:没有特别的需要,不要使用。

public class ArrayUtil2 {
	private ArrayUtil2() {
	}

	private static volatile ArrayUtil2 instance = null;

	// 同步方法:此时的同步监听对象是(ArrayUtil2.class)
	public static ArrayUtil2 getInstance() {
		if (instance == null) {
			synchronized (ArrayUtil2.class) {
				if (instance == null)
					instance = new ArrayUtil2();
			}
		}
		return instance;
	}

	public void sort(int[] arr) {

	}
}

方式三: 锁机制(Lock)
锁(Lock):
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作。Lock具有上述所有功能,而且更强大,更体现面向对象。

class Apple3 implements Runnable {
	private int num = 50;// 苹果总数

	private final Lock lock = new ReentrantLock();// 创建锁对象

	public void run() {
		for (int i = 1; i <= 50; ++i) {
			eat();
		}
	}

	private void eat() {
		// 同步代码块
		// 进入方法立刻加锁
		lock.lock();//获取锁
		try {
			if (num > 0) {
				System.out.println(Thread.currentThread().getName() 
									+ "吃了编号为" + num + "的苹果");
				Thread.sleep(20); // 模拟网络延迟
				num--;
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		} finally {
			lock.unlock();//释放锁
		}
	}
}

线程通信

不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信,协调完成工作。经典的是生产者和消费者案例(Producer/Consumer):

ShareResource.java

//共享资源对象(姓名-性别)
public class ShareResource {
	private String name;
	private String gender;

	/**
	 * 生产者向共享资源对象中存储数据
	 * 
	 * @param nmae
	 * @param gender
	 */
	synchronized public void push(String name, String gender) {
		this.name = name;
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.gender = gender;
	}

	/**
	 * 消费者从共享对象中取出资源
	 */
	synchronized public void popup() {
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(this.name + "-" + this.gender);
	}
}

Producer.java

//生产者
public class Producer implements Runnable {
	private ShareResource resource = null;

	public Producer(ShareResource resource){
		this.resource = resource;
	}
	
	public void run() {
		for (int i = 0; i < 50; i++) {
			if (i % 2 == 0) {
				resource.push("Bob", "男");
			} else {
				resource.push("Sue", "女");
			}
		}
	}
}

Consumer.java

//消费者
public class Consumer implements Runnable {
	private ShareResource resource = null;

	public Consumer(ShareResource resource){
		this.resource = resource;
	}
	public void run() {
		for (int i = 0; i < 50; i++) {
			resource.popup();
		}
	}
}

APP.java

//测试代码
public class App {

	public static void main(String[] args) {
		//创建生产者和消费者共同的资源对象
		ShareResource resource = new ShareResource();
		//启动生产者线程
		new Thread(new Producer(resource)).start();
		//启动消费者线程
		new Thread(new Consumer(resource)).start();
	}
}

这样会导致性别紊乱,而且并不是生产一个数据消费一个数据。
前者我们可以使用线程同步解决,后者,需要介绍一个概念:
同步锁池:
同步锁必须选择多个线程共同的资源对象

当前生产者在生产数据的时候(先拥有同步锁),其他线程就会在锁池中等待获取锁,当线程执行完同步代码块时,就会释放同步锁,其他锁开始抢锁的使用权。

线程通信 wait-notify方法介绍(等待和唤醒机制)
java.lang.Object类提供两类,用于才做线程通信的方法。
wait(): 执行该方法的线程对象,释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程。
notify(): 执行该方法的线程,唤醒等待池中等待的任意一个线程,把线程转到锁池中等待。
notifyAll(): 执行该方法的线程,唤醒等待池中所有线程,把线程转到锁池中等待。
注意: 上述方法只能被同步监听锁对象来调用,否则报错IllegalMonitorStateException

多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果。我们把这个用来做互斥的对象称之为:同步监听对象或者同步锁
同步锁对象可以是任意类型的对象,只要保证多个线程使用的是相同的锁对象即可。
因为只有同步监听锁对象才能调用wait和notify方法,所以,wait和notify方法应该存在于Object类中,而不是Thread类中。


下面以线程A和线程B共同使用X对象(同步锁),用wait和notify方法进行通信进行具体阐释:

1.当线程A执行X对象的同步方法时,A线程持有X对象的锁,B线程没有执行X对象的机会,B线程在X对象的锁池中进行等待;
2.A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,A线程进入X对象的等待池中;
3.在X对象的锁池中等待锁的B线程获取X对象的锁,执行X对象的另一个同步方法;
4.B线程在同步方法中调用X.notify()方法,JVM把A线程从X对象的等待池中移动到X对象的锁池中,等待获取锁;
5.B线程执行完同步方法,释放锁,A线程获得锁,继续执行同步方法。
…以此类推…


下面是改进过后的ShareResource类:

//共享资源对象(姓名-性别)
public class ShareResource {
	private String name;
	private String gender;
	private boolean isEmpty = true;

	/**
	 * 生产者向共享资源对象中存储数据
	 * 
	 * @param nmae
	 * @param gender
	 */
	synchronized public void push(String name, String gender) {
		try {
			while (!isEmpty) { // 当isEmpty为false时,表明资源对象不空,等待消费者来获取
				// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池中,只能被其他线程唤醒
				this.wait();
			}
			// -----生产开始------
			this.name = name;
			Thread.sleep(10);
			this.gender = gender;
			// -----生产结束------
			isEmpty = false;// 设置共享资源数据不为空
			this.notify();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	/**
	 * 消费者从共享对象中取出资源
	 */
	synchronized public void popup() {
		try {
			while (isEmpty) {// 当isEmpty为true时,表明资源对象为空,等待生产者来生产
				// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池中,只能被其他线程唤醒
				this.wait();//唤醒一个消费者
			}
			// -----消费开始------
			System.out.println(this.name + "-" + this.gender);
			Thread.sleep(10);
			// -----消费开始------
			isEmpty = true;// 设置共享资源数据为空
			this.notify();//唤醒一个生产者
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

使用Lock和Condition接口
wait和notify方法只能被同步监听锁对象来调用,否则报错IllegalMonitorStateException
但是Lock机制根本就没有同步锁,也就没有办法自动获取锁和自动释放锁的概念,也就没有办法调用wait和notify方法。
解决方法:使用Condition接口中的await和signal/signalAll方法。

public class ShareResource {
	private String name;
	private String gender;
	private boolean isEmpty = true;
	private final Lock lock = new ReentrantLock();
	private Condition condition = lock.newCondition();

	synchronized public void push(String name, String gender) {
		lock.lock();
		try {
			while (!isEmpty) {
				condition.await();
			}
			this.name = name;
			Thread.sleep(10);
			this.gender = gender;
			condition.signal();
			isEmpty = false;
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	synchronized public void popup() {
		lock.lock();
		try {
			while (isEmpty) {
				condition.await();
			}
			System.out.println(this.name + "-" + this.gender);
			Thread.sleep(10);
			isEmpty = true;
			condition.signal();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
}

死锁

多线程通信的时候很容易造成死锁。死锁无法解决,只能避免。
当A线程等待由B线程持有的锁,而B线程等待A线程持有的锁时,发生死锁现象,JVM不检测也不去避免这种情况的发生,所以程序员必须保证不发生死锁现象。

避免死锁法则:当多个线程都要访问同一个共享资源时,保证每一个线程都按照相同的顺序去访问。

比如哲学家吃面条的故事:
哲学家就餐问题
(来自百度百科)

线程的生命周期

生命周期: 线程也是有生命周期的,也是存在不同的状态的,并且状态之间可以互相转化。
线程的生命周期
官方API文档给出的分类:
这里写图片描述
有人把阻塞状态,等待状态,计时等待状态合称为阻塞状态,如下图:

这里写图片描述

线程对象的状态存放在Thread类的内部类中(State)
注意: State实际上是一个枚举类,因为线程对象的状态是固定的,只有6种,因此用枚举是最恰当的。

线程状态:

1.新建状态(new):
使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前。
新建状态下,线程压根就没有启动,仅仅是存在一个线程对象而已。
Thread t = new Thread();就属于一个新建状态
当新建状态下的线程调用了start方法后,线程进入可运行状态(start方法只能调用一次,否则报错IllegalMonitorStateException)。

2.可运行状态(Runnable): 可分为两种,ready和running,分别表示就绪和运行状态。
就绪状态:线程对象调用start方法后,等待JVM的调度(此时该线程并没有运行)
运行状态:线程获得JVM调度,如果存在多个CPU,允许多个线程同时运行。
可运行状态
3.阻塞状态(blocked):
正在运行的线程因为某些情况放弃CPU,暂时停止运行,就会进入阻塞状态。
此时JVM不会给线程分配CUP,直到线程重新进入就绪状态,才有机会转到运行状态。

阻塞状态的两种情况:
1):当A线程处于运行过程时,试图获取同步锁时,却被B线程获取,此时JVM把A线程存到对象的锁池中,A线程进入阻塞状态;
2):当现场处于运行过程时,发出了IO请求,此时进入阻塞状态。

4.等待状态(waiting):(等待状态只能被其他线程唤醒)
1):当线程处于运行过程时,调用了wait方法,此时JVM把当前线程存入对象的等待池中;
2):当前线程执行了sleep方法;

5.计时等待状态(timed waiting):
1):当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存入对象的等待池中;
2):当前线程执行了sleep(long time)方法。

6.终止状态(terminated):通常称为线程死亡状态,表明线程终止
1):执行完run方法而退出(正常死亡);
2):遇到异常而退出(非正常死亡)。

线程一旦死亡,就不能再次启动,否则报错(IllegalMonitorStateException)

在Thread类中因为存在线程安全问题,已经过时的方法:
void suspend() //暂停当前线程
void resume() //恢复当前线程
void stop() //结束当前线程

线程的几种操作

线程睡眠:
让执行的线程暂停一段时间,进入计时等待状态。
调用sleep之后,当前线程放弃CPU,在指定时间段内,sleep所在线程不会获得执行机会,此状态下的线程不会释放同步锁/同步监听器。
该方法更多的模拟网络延迟,让多线程并发访问一个资源的错误更明显。

联合线程:
线程的join方法表示一个线程等待另一个线程完成后才执行。join方法被调用后,线程对象处于阻塞状态。

后台线程:
在后台运行,其目的是为其他线程提供服务,也称为“守护线程”。JVM的垃圾回收线程就是典型的后台线程。
**特点:**若所有的前台线程死亡,后台线程自动死亡,前台线程没有结束,后台线程不会结束。
测试对象是否为后台线程可以用:Thread.isDaemon() 方法。
一般默认创建的是前台线程,可以用setDaemon() 方法来设置为后台线程,并且当且仅当后台线程创建的新线程时,新线程是后台线程。
设置后台线程:Thread.setDaemon(true) ,该方法必须在start方法调用前,否则出现IllegalMonitorStateException异常。

class DaemonThread extends Thread {
	public void run() {
		for (int i = 0; i < 500; i++) {
			System.out.println(super.getName() + "-" + i);
		}
	}
}

public class DaemonDemo {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());

		for (int i = 0; i < 50; i++) {
			System.out.println("main" + "-" + i);
			if (i == 10) {
				DaemonThread t = new DaemonThread();
				t.setDaemon(true);
				t.start();
			}
		}
	}
}

线程的优先级:
每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关,并非线程的优先级越高就一定先执行,哪个线程先运行取决于CPU的调度。
MAX_PRIORITY = 10; //最高优先级
MIN_PRIORITY = 1; //最低优先级
NORM_PRIORITY = 5; //默认优先级
注意: 每个线程有默认优先级,主线程默认优先级为5,由A线程创建的B线程也具有和A线程一样的优先级。不同的操作系统支持的线程优先级不同,建议使用上述的三种优先级。

线程礼让:
yield方法:表明当前线程对象提示调度器自己愿意让出CPU资源,但是调度器可以忽视该请求。
调用该方法后,线程进入就绪状态,所以有可能线程执行该方法之后,调度器又将其调度出来
从Java7的API中可以清楚的看出,开发中很少使用该方法,该方法主要用于调试或测试,它可能有助于因多线程竞争条件下的错误重现现象。
sleep方法和yield方法的区别:
1)都能使当前线程放弃CPU,把运行机会给其他线程;
2)sleep方法会给其他线程机会,但不会考虑其他线程的优先级,而yield会给优先级最高的线程执行;
3)调用sleep方法后,线程进入等待状态,调用yield方法后,线程进入就绪状态。

线程定时器和线程组:
定时器:
java.util包中提供了Timer类,实现定时执行任务

public static void main(String[] args) {
		Timer timer = new Timer();
		timer.schedule(new TimerTask(){
			public void run(){
				System.out.println(new Date().toLocaleString());
			}
		}, 3000,1000);
	}

线程组:
ThreadGroup类表示线程组,可以对一组线程进行集中管理。
用户在创建线程对象时,可以通过构造器指定其所在的线程组。
Thread(ThreadGroup group,String name);
如果A线程创建了B线程,如果没有设置B线程的分组,那么B线程加入到A线程的线程组。
一旦线程加入某个线程组,中途不能修改线程分组。

当Java运行时,JVM会创建名为main的线程组,在默认情况下,所有线程都在该线程组下。


至此,线程最基本的知识点结束了,当然线程远不止这么些内容,比如:
1.线程安全集合对象;
2.ThreadLocal:本地线程变量,ThreadLocal模式;
3.线程池,Java8新增的线程池功能;
4.Java5新增的线程并发操作,等等等等

Java的学习不能毕其功于一役,而是应该日积月累,打好基础,深入理解编程语言的内涵,参透原理才是学习编程语言的最终目的,并不是Ctrl C ,Ctrl V代码就是自己的了。

此文一万五千多字,全部手打,包括代码(在Eclipse中敲完复制过来的),结构可能有些模糊不清,还请谅解。
这样虽然花了两天时间,但是印象更加深刻,对于基础知识的掌握会更加牢固。

更多内容期待日后再更。
(2018.8.16-2018.8.18)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值