Java 多线程基础

Posted 将暮未暮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 多线程基础相关的知识,希望对你有一定的参考价值。

1.多线程基础

1)进程         

  进程(process)就是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元成为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或者首要线程的线程。操作系统中有若干个线程再“同时”运行。通常,操作系统上运行的每一个应用程序都运行在一个进程中,例如QQ、IE等。

  进程并不是真正意义上的同时运行,而是并发运行。

2)线程

  一个线程是进程的一个顺序执行流。同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行的堆栈。线程再切换时负荷小,因此线程也被成为轻负荷进程,一个进程中可以包含多个线程。

  切换:线程并发时的一种现象。

3)进程与线程的区别

  一个进程至少有一个线程,线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大的提高了程序的运行效率。

  线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

  从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。

4)线程使用场合

  线程通常用于在一个程序中需要同时完成多个任务的情况,我们可以将每个任务定义为一个线程,使他们得以一同工作。

  例如我们在玩某个游戏时,这个游戏由操作系统运行,所以其运行在一个独立的进程中,而在游戏中我们会听到某些背景音乐、某个角色在移动、出现某些绚丽的动画效果等,这些在游戏中都是同时发生的,但实际上,播放音乐是在一个线程中独立完成的,移动某个角色,播放某些特效也都是在独立的线程中完成的,这些事情我们无法在单一线程中完成。

  也可以用于在单一线程中可以完成,但是使用多线程可以更快的情况,比如下载文件,如迅雷,通常会打开多个节点来同时下载一个文件。

5)并发原理

  多个线程或进程“同时”运行只是我们感官上的一种表现,事实上进程和线程是并发运行的。OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行,围观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但不是绝对意义上的“同时发生”。

  之所以这样做是因为CPU只有一个,同一时间只能做一件事,但随着计算机的发展,出现了多核心CPU,例如两核心CPU可以实现真正意义上的2线程同时运行,但因为CPU的时间片段分配给哪个进程或线程是由线程调度决定,所以不一定两个线程都是属于同一个进程的,无论如何我们只需要知道线程或进程是并发运行即可。

  OS:Operating System 操作系统。

  线程调度机制是OS提供的一个用于并发处理的程序,Java虚拟机自身也提供了线程调度机制,用于减轻OS切换线程带来的更多负担。

6)线程状态

  对于程序而言,我们实际上关心的是线程而非进程,线程在其生命周期中的各个状态如下:

  • New:当我们创建一个线程时,该线程并没有纳入线程调度,其处于一个new状态。
  • Runnable:当调用线程的start方法后,该线程纳入线程调度的控制,其处于一个可运行状态,等地分配时间片以并发运行。
  • Running:当线程被分配到了时间片段后其被CPU运行,这时该线程处于running状态。
  • Blocked:当线程在运行过程中可能会出现阻塞现象,比如等待用户输入信息等,但堵塞状态不是百分百出现的,具体要看代码中是否有相关需求。
  • Dead:当线程的任务全部运行完毕,或在运行过程中抛出了一个未捕获的异常,那么线程结束,等待GC回收。

 

2.创建线程

1)使用Thread创建线程并启动线程

   Thread类是线程类,其每一个实例表示一个可以并发运行的线程。我们可以通过继承该类并重写run方法来定义一个具体的线程。其中重写run方法的目的是定义该线程要执行的逻辑。启动线程时调用线程的start()方法而非直接调用run()方法。start()方法会将当前线程纳入线程调度,使当前线程可以开始并发运行。当线程获取时间片段后会自动开始执行run方法中的逻辑。

案例1:创建线程方法一:继承线程Thread并重写run

public class Thread_currentThread {
	public static void main(String[] args) {
		Thread t1 = new Mythread1();  //线程实例化
		Thread t2 = new Mythread2();
		
		t1.start();
		t2.start();
	}
}
class Mythread1 extends Thread{  //创建方法继承Thread
	public void run(){    //重写run方法
		for(int i=0;i<1000;i++){
			System.out.println("线程1");
		}
	}
}
class Mythread2 extends Thread{
	public void run(){
		for(int i=0;i<1000;i++){
			System.out.println("线程2");
		}
	}
}

 

2)使用Runnable创建并启动线程

  实现Runnable接口并重写run方法来定义线程体,然后在创建线程的时候将Runnable的实例传入并启动线程。

  这样做的好处在于可以将线程与线程要执行的任务分离开减少耦合,同时Java是单继承的,定义一个类实现Runnable接口这样的做法可以更好的去实现其他父类或接口,因为接口是多继承的。

案例2:创建线程方法二:实现Runnable接口单独定义线程任务

public class RunnableDemo {
	public static void main(String[] args) {
		Runnable run1 = new MyRunnable1();
		Runnable run2 = new MyRunnable2();
		
		Thread t1 = new Thread(run1);
		Thread t2 = new Thread(run2);
		
		t1.start();
		t2.start();
	}
}
class MyRunnable1 implements Runnable{
	public void run(){
		for(int i=0;i<1000;i++){
			System.out.println("线程1");
		}
	}
}
class MyRunnable2 implements Runnable{
	public void run(){
		for(int i=0;i<1000;i++){
			System.out.println("线程2");
		}
	}
}

  

3)使用内部类创建线程

  通常我们可以通过匿名内部类的方式创建线程,使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这种方式来创建。

//用方式一和方式二,匿名内部类的方式创建两个线程

public class ThreadDemo {
	public static void main(String[] args) {
		//方式一:继承Thread,重写run
		Thread t1 = new Thread(){
			public void run(){
				for(int i=0;i<1000;i++){
					System.out.println("线程1");
				}
			}
		};
		//方式二:Runnable方式
		Runnable r2 = new Runnable(){
			public void run(){
				for(int i=0;i<1000;i++){
					System.out.println("线程2");
				}
			}
		};
		Thread t2 = new Thread(r2);
		
		t1.start();
		t2.start();
	}
}

  

3.线程操作API

1)Thread.currentThread方法

  Thread的静态方法currentThread方法可以用于获取运行当前代码的线程。

/*
 * 线程提供了一个静态方法:
 * static Thread currentThread()
 * 该方法可以获取运行当前方法的线程
 */
public class ThreadcurrentThreadDemo {
	public static void main(String[] args) {
		//获取main方法的线程
		Thread main = Thread.currentThread();
		System.out.println("运行main方法的线程是:"+main);
		
		Thread t = new Thread(){
			public void run(){
				Thread t = Thread.currentThread();
				System.out.println("自定义线程是:"+t);
				ct();
			}
		};
		ct();
		t.start();
	}
	public static void ct(){
		Thread t = Thread.currentThread();
		System.out.println("运行ct方法的线程是"+t);
	}
}

/*
运行结果:
运行main方法的线程是:Thread[main,5,main]
运行ct方法的线程是Thread[main,5,main]
自定义线程是:Thread[Thread-0,5,main]
运行ct方法的线程是Thread[Thread-0,5,main]
*/

  

2)获取线程信息

  Thread提供了获取线程信息的相关方法:

  • long getId():返回该线程的标识符
  • String getName():返回该线程的名称
  • int getPriority():返回线程的优先级
  • Thread.state getState():获取线程的状态
  • boolean isAlive():测试线程是否处于活动状态
  • boolean isDaemon():测试线程是否为守护线程
  • boolean isInterrupted():测试线程是否已经中断
public class Thread_info {
	public static void main(String[] args) {
		//获取main方法的线程
		Thread t = Thread.currentThread();
		
		long id = t.getId();
		System.out.println("id:"+id);
		
		String name = t.getName();
		System.out.println("name:"+name);
		
		int priority = t.getPriority();
		System.out.println("优先级:"+priority);
		
		boolean isAlive = t.isAlive();
		System.out.println("isAlive"+isAlive);
		
		boolean isDaemon = t.isDaemon();
		System.out.println("isDaemon:"+isDaemon);
		
		boolean isInterrupted = t.isInterrupted();
		System.out.println("isInterrupted:"+isInterrupted);
	}
}

/*
运行结果:
id:1
name:main
优先级:5
isAlivetrue
isDaemon:false
isInterrupted:false
*/

  

3)线程优先级

  线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片的几率。

  线程的优先级被分为10级,值为1-10,10最高。线程提供了3个常量表示最低,最高以及默认状态。

  • Thread.MIN_PRIORITY
  • Thread.MAX_PRIORITY
  • Thread.NORM_PRIORITY

  设置优先级方法:

void setPriority(int priority)

 

public class Thread_setPriority {
	public static void main(String[] args) {
		Thread max = new Thread(){
			public void run(){
				for(int i=0;i<1000;i++){
					System.out.println("max");
				}
			}
		};
		Thread norm = new Thread(){
			public void run(){
				for(int i=0;i<10000;i++){
					System.out.println("nor");
				}
			}
		};
		Thread min = new Thread(){
			public void run(){
				for(int i=0;i<10000;i++){
					System.out.println("min");
				}
			}
		};
		
		max.setPriority(Thread.MAX_PRIORITY);
		min.setPriority(Thread.MIN_PRIORITY);
		min.start();
		norm.start();
		max.start();
	}
}

  

4)守护线程

  守护线程与普通线程在表现上没有什么却别,我们只需要通过Thread提供的方法来设定即可:

void setDaemon(boolean)

  当参数为true时该线程为守护线程,守护线程的特点是:当进程中只剩下守护线程时,所有守护线程强制终止。

  GC就是运行在一个守护线程上的。

  需要注意点是:设置线程为后台线程要在该线程启动前设置。

Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
deamonThread.start();

案例3:创建守护进程

public class Thread_setDaemon {
	public static void main(String[] args) {
		Thread rose = new Thread(){
			public void run(){
				for(int i=0;i<3;i++){
					System.out.println("rose:let me jump");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("音效:噗通");
			}
		};
		Thread jack = new Thread(){
			public void run(){
				System.out.println("jack:you jump I jump");
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
		
		rose.start();
		jack.setDaemon(true);  //要在start之前设置为后台进程
		jack.start();
	}
}

  

5)sleep方法

  Thread的静态方法sleep用于使当前线程进入堵塞状态:

static void sleep(long ms)

  该方法会使当前线程进入堵塞状态指定毫秒,当指定毫秒堵塞后,当前线程会重新进入Runnable状态,等待分配时间片。该方法声明抛出一个InterrupException,所以在使用该方法时需要捕获这个异常。

案例4:电子钟程序

要求:每1s中在控制台显示当前时间

import java.text.SimpleDateFormat;
import java.util.Date;

public class Thread_sleep {
	public static void main(String[] args) {
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
		while(true){
			Date date = new Date();
			System.out.println(sdf.format(date));
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

 

6)join方法

  join语法:

void join()

  该方法用于等待当前线程结束。此方法是一个阻塞方法。该方法声明抛出InterruptException。

案例5:join()方法演示

/*
 * 线程提供了join()方法,该方法允许多线程之间同步执行
 * 
 * 同步:有先后顺序的执行代码
 * 异步:各干各的
 * 
 * 演示:下载图片,图片下载完毕之后显示图片
 */
public class Thread_join {
	public static boolean isFinish;  //表示图片是否下载完毕
	public static void main(String[] args){
		final Thread download = new Thread(){
			public void run(){
				System.out.println("down:开始下载图片....");
				for(int i=10;i<=100;i+=10){  //模拟图片下载百分比
					System.out.println("down:"+i+"%");
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
				System.out.println("down:下载图片完毕!");
				isFinish = true;
			}
		};
		Thread show = new Thread(){
			public void run(){
				System.out.println("show:开始显示图片...");
				//首先等待图片下载完成
				try {
					download.join();  //show线程开始堵塞,知道download线程的run方法执行完毕
				} catch (InterruptedException e) {
					e.printStackTrace();
				}  
				if(!isFinish){
					throw new RuntimeException("show:图片没有下载完毕!");
				}
				System.out.println("show:图片显示完毕!");
			}
		};
		download.start();
		show.start();
	}
}

/*
运行结果:
show:开始显示图片...
down:开始下载图片....
down:10%
down:20%
down:30%
down:40%
down:50%
down:60%
down:70%
down:80%
down:90%
down:100%
down:下载图片完毕!
show:图片显示完毕!
*/

 

7)yield方法

  Thread的静态方法yield语法:

static void yield()

  该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片。

 

4.线程同步

1)synchronized关键字

  多个线程并发读取同一个临界资源时会发生“线程并发安全问题”。

  常见的临界资源:

  • 多线程共享实例变量
  • 多线程共享静态公共变量

  若想解决线程安全问题,需要将异步的操作变为同步操作。

  • 异步:多线程并发的操作,相当于各干各的。
  • 同步:有先后顺序的操作,相当于你干完我再干。

  Java中关键字:synchronized是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。

案例6:模拟线程并发安全问题

/*
 * 多线程并发安全问题:
 * 当多个线程并发访问同一数据时,由于线程切换时机的不正确,
 * 导致代码未按照代码设计顺序执行而出现混了,严重时可能导致系统瘫痪。
 * 
 * 假设桌上有20个豆子,两个线程分别模拟两个人去抓取豆子,这样在最后一颗
 * 豆子时,如果线程1取走了而正好在豆子归零的时候分配的时间片到时,发生线程
 * 切换进入Runnable状态等待重新分配时间片,而此时线程2进入,发现还有一个豆子,
 * 就取走了,这样就造成了问题,最后一个豆子被取走了两次。
 * 模拟上述情况,有一定几率发生死循环,就是最后一个豆子被取走两次,豆子数量为负数的时候。
 */
public class Thread_SyncDemo01 {
	public static void main(String[] args) {
		final Table table = new Table();
		Thread t1 = new Thread(){
			public void run(){
				int bean = table.getBean();
				Thread.yield();
				System.out.println(getName()+":"+bean);
			}
		};
		Thread t2 = new Thread(){
			public void run(){
				while(true){
					int bean = table.getBean();
					Thread.yield();
					System.out.println(getName()+":"+bean);
				}
			}
		};
		t1.start();
		t2.start();
	}
}
class Table{
	//桌上有20个豆子
	private int beans = 20;
	public int getBean(){  //取豆子方法
		if(beans==0){  //当剩下0个豆子时抛出异常
			throw new RuntimeException("没有豆子了");
		}
		//模拟线程执行到这里发生了切换
		Thread.yield();
		return beans--;  ///每取走一颗豆子beans减1
	}
}

/*
运行结果:
异常状态:
Thread-1:-656273
Thread-1:-656274
Thread-1:-656275
Thread-1:-656276
Thread-1:-656277
Thread-1:-656278
Thread-1:-656279
Thread-1:-656280
Thread-1:-656281
正常状态:
Thread-0:20
Thread-1:19
Thread-1:18
Thread-1:17
Thread-1:16
Thread-1:15
Thread-1:14
Thread-1:13
Thread-1:12
Thread-1:11
Thread-1:10
Thread-1:9
Thread-1:8
Thread-1:7
Thread-1:6
Thread-1:5
Thread-1:4
Thread-1:3
Thread-1:2
Thread-1:1
Exception in thread "Thread-1" java.lang.RuntimeException: 没有豆子了
	at javase.day09.Table.getBean(Thread_SyncDemo01.java:41)
	at javase.day09.Thread_SyncDemo01$2.run(Thread_SyncDemo01.java:26)
*/

 

2)锁机制

  Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized关键字),同步代码块包含两部分:

  • 一个作为锁的对象的引用
  • 一个作为由这个锁保护的代码块
sysnchronized(同步监视器——锁对象引用){
    代码块;
}

  若方法所有代码都需要同步也可以给方法直接枷锁。

  每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时释放锁,而且无论是通过正常路径退出还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。

3)选择合适的锁对象

  使用synchronized需要一个锁对象上锁以保护线程同步。

  那么这个锁对象应当注意:多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象引用。否则达不到同步效果,通常会使用this来作为锁对象。

4)选择合适的锁范围

  在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。

5)静态方法锁

public synchronized static void dosome(){
    ...
}

  当对一个静态方法枷锁,那么该方法锁的对象是类对象,每个类都有唯一的一个类对象,获取类对象的方式:类名.class。静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。因为静态方法锁的是类对象而非静态方法锁的当前方法所属对象。

 

案例7:synchronized修饰(同步)方法演示

public class Thread_SyncDemo01 {
	public static void main(String[] args) {
		final Table table = new Table();
		Thread t1 = new Thread(){
			public void run(){
				int bean = table.getBean();
				Thread.yield();
				System.out.println(getName()+":"+bean);
			}
		};
		Thread t2 = new Thread(){
			public void run(){
				while(true){
					int bean = table.getBean();
					Thread.yield();
					System.out.println(getName()+":"+bean);
				}
			}
		};
		t1.start();
		t2.start();
	}
}
class Table{
	private int beans = 20;
	/*
	 * 当一个方法被synchronized修饰后,那么该方法成为“同步方法”
	 * 即:多个线程不能同时进入方法内部执行
	 * 在方法上直接使用synchronized,那么同步监视器对象就是当前
	 * 方法所属对象,即方法内部看到的“this”
	 */
	public synchronized int getBean(){  //使用synchronized修饰方法
		if(beans==0){  //当剩下0个豆子时抛出异常
			throw new RuntimeException("没有豆子了");
		}
		//模拟线程执行到这里发生了切换
		Thread.yield();
		return beans--;
	}
}

/*
 运行结果:
 Thread-0:20
Thread-1:19
Thread-1:18
Thread-1:17
Thread-1:16
Thread-1:15
Thread-1:14
Thread-1:13
Thread-1:12
Thread-1:11
Thread-1:10
Thread-1:9
Thread-1:8
Thread-1:7
Thread-1:6
Thread-1:5
Thread-1:4
Thread-1:3
Thread-1:2
Thread-1:1
Exception in thread "Thread-1" java.lang.RuntimeException: 没有豆子了
	at javase.day09.Table.getBean(Thread_SyncDemo01.java:46)
	at javase.day09.Thread_SyncDemo01$2.run(Thread_SyncDemo01.java:26)

*/

  

案例8:synchronized同步代码块

/*
* 模拟买衣服的情况,很多人可以进去选衣服,但是试衣服时,当试衣间有人时,需要排队,
* 这样线程就不能同时运行。
* 
* 同步块:有效的缩小同步范围可以在保证并发安全的前提下提高代码执行效率
* 同步块可以更佳精确的控制需要同步的代码片段
* */
public class Thread_SyncDemo02 {
	public static void main(String[] args) {
		final Shop shop = new Shop();
		Thread t1 = new Thread(){
			public void run(){
				shop.buy();
			}
		};
		Thread t2 = new Thread(){
			public void run(){
				shop.buy();
			}
		};
		t1.start();
		t2.start();
	}
}
class Shop{
	public void buy(){
		Thread t = Thread.currentThread();
		System.out.println(t.getName()+":正在挑选衣服...");
		try {
			Thread.sleep(2000);
		/*
		 * 同步块需要指定监视器对象(上锁的对象),只要保证该对象
		 * 多个线程看到的是同一个,那么这些线程就不会同时执行同步块中的代码
		 */
		synchronized(this){
			System.out.println(t.getName()+":正在试衣服...");
				Thread.sleep(2000);
			System.out.println(t.getName()+":试衣服结束!");
		}
		System.out.println(t.getName()+":结账离开!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

/*
运行结果:
Thread-1:正在挑选衣服...
Thread-0:正在挑选衣服...
Thread-1:正在试衣服...
Thread-1:试衣服结束!
Thread-0:正在试衣服...
Thread-1:结账离开!
Thread-0:试衣服结束!
Thread-0:结账离开!
*/

  

案例9:synchronized修饰静态方法

/*
 * 静态方法上使用synchronized修饰,那么该方法一定具有同步效果
 * 静态方法的同步监视器对象为当前静态方法所属类的类对象(Class的实例)
 * JVM在加载每一个类的时候都会实例化一个且只会实例化一个Class的实例来
 * 描述它,所以每个类有且只有一个Class的实例,静态方法锁的就是这个对象
 */
public class Thread_SyncDemo03 {
	public static void main(String[] args) {
		Thread t1 = new Thread(){
			public void run(){
				Sync.dosome();
			}
		};
		Thread t2 = new Thread(){
			public void run(){
				Sync.dosome();
			}
		};
		t1.start();
		t2.start();
	}
}

class Sync{
	public synchronized static void dosome(){
		Thread t = Thread.currentThread();
		System.out.println(t.getName()+":正在运行dosome方法");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(t.getName()+":dosome方法运行完毕!");
	}
}

/*
运行结果:
Thread-0:正在运行dosome方法
Thread-0:dosome方法运行完毕!
Thread-1:正在运行dosome方法
Thread-1:dosome方法运行完毕!
*/

  

案例10:互斥锁

/*
 * 互斥锁
 * 当使用synchronized修饰多段不同代码时,同步监视器对象相同,
 * 那么这些代码间就存在互斥,即:多个线程不能同时执行这些代码
 */
public class Thread_SyncDemo04 {
	public static void main(String[] args) {
		final Aoo aoo = new Aoo();
		Thread t1 = new Thread(){
			public void run(){
				aoo.methodA();
			}
		};
		Thread t2 = new Thread(){
			public void run(){
				aoo.methodB();
			}
		};
		t1.start();
		t2.start();
	}
}
class Aoo{
	public synchronized void methodA(){   //修饰方法
		Thread t = Thread.currentThread();
		System.out.println(t.getName()+":正在运行方法A...");
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println(t.getName()+":方法A运行完毕!");
	}
	public void methodB(){
		synchronized (this) {   //修饰代码块
			Thread t = Thread.currentThread();
			System.out.println(t.getName()+":正在运行方法B...");
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(t.getName()+":方法B运行完毕!");
		}
	}
}

/*
运行结果:
Thread-0:正在运行方法A...
Thread-0:方法A运行完毕!
Thread-1:正在运行方法B...
Thread-1:方法B运行完毕!
*/

  

6)wait和notify

  多线程之间需要协调工作。如:浏览器的一个显示图片的displayThread想要执行显示图片的任务,必须等待下载线程downloadThread将该图片下载完毕,如果图片还没有下载完毕,displayThread可以暂停,当下载完毕再通知displayThread显示图片,这时displayThread线程继续执行。

  当条件不满足,则线程等待。当条件满足,则线程将被唤醒。这个机制得依赖wait和notify,等待机制与锁机机制是密切关联的。

 

5.线程池

1)线程安全API与非线程安全API

  StringBuffer是同步的synchronized append();

  StringBuilder不是同步的append();

  StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的,当不存在并发时首选应当使用StringBuilder。

  Vector和Hashtable是线程安全的,而ArrayList和HashMap则不是线程安全的。

  对于集合而言,Collection是提供了几个静态方法,可以将集合或Map转换为线程安全的。

  • Collections.synchronizedList():获取线程安全的List集合
  • Collections.synchronizedMap():获取线程安全的Map

案例11:线程安全API演示

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/*
 * 将集合和Map转换为线程安全的
 * List常用实现类:ArrayList,LinkedList
 * Set常用实现类:HashSet
 * Map常用实现类:HashMap
 * 以上都不是线程安全的
 * 
 * 可以通过集合工具类:Collections将他们转换为线程安全的 
 */
public class ThreadApiDemo {
	public static void main(String[] args) {
		List<String> list = new ArrayList<String>();
		list.add("one");
		list.add("two");
		list.add("three");
		System.out.println(list);
		/*
		 * 将给定List集合转换为线程安全的
		 */
		list = Collections.synchronizedList(list);
		System.out.println("线程安全的List:"+list);
		
		Set<String> set = new HashSet<String>(list);  //将List内容转换为Set,注意不能有重复元素,如果有重复元素则只保留一份
		System.out.println(set);
		/*
		 * 将Set集合转换为线程安全的
		 */
		set = Collections.synchronizedSet(set);
		System.out.println("线程安全的Set:"+set);
		
		Map<String,Integer> map = new HashMap<String,Integer>();
		map.put("语文",90);
		map.put("数学",100);
		map.put("英语", 60);
		System.out.println(map);
		/*
		 * 将Map转换为线程安全的
		 */
		map = Collections.synchronizedMap(map);
		System.out.println("线程安全的Map:"+map);
	}
}

/*
运行结果:
[one, two, three]
线程安全的List:[one, two, three]
[one, two, three]
线程安全的Set:[one, two, three]
{数学=100, 语文=90, 英语=60}
线程安全的Map:{数学=100, 语文=90, 英语=60}
*/

  

2)ExecutorService实现线程池

  当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度资源消耗,以及过度切换线程的危险,从而可能导致系统崩溃,所以需要使用线程池来解决这个问题。ExecutorService是Java提供的用于管理线程池的类。

  线程池的作用:1.控制线程数量;2.重用线程。

  线程池的概念:首先创建一些线程,他们的集合成为线程池,当服务器接收到一个客户请求后,就从线程池中取出空闲的线程为之服务,服务完毕后不关闭该线程,而是将该线程还回线程池中。

  在线程池的编程模式下,任务是提交给整个线程池,而不是提交给某个线程,线程池在拿到任务后,它就在内部找空闲的线程再把任务交给内部空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

  线程池有以下集中实现策略:

  • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池,但是以前构造的线程可用时将重用它们。
  • Executors.newFixedThreadPool(int nThread):创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
  • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行或者定期的执行。
  • Executors.newSingleThreadExecutor():创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。
//创建一个固定线程数量的线程池:

...
    ExecutorService threadPool 
        = Executors.newFixedThreadPool(30);//创建具有30个线程的线程池
    Runnable r1 = new Runable(){
        public void run(){
            //线程体
        }
    };
    threadPool.execute(r1);//将任务交给线程池,其会分配空闲线程来运行这个任务。
    ...

  

3)使用BlockingQueue

  BlockingQueue是双缓冲队列。

  在多线程并发时,若需要使用队列,可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。

  BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。

  双缓冲队列有以下几种实现:

  • ArrayBlockingDeque:规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小,其所含的对象是以FIFO(先进先出)顺序排序的。
  • LinkedBlockingDeque:大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,生成的BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的大小由Integer.MAX_VALUE来决定,其所含的对象是以FIFO(先进先出)顺序排序的。
  • PriorityBlockingDeque:类似于LinkedBlockingDeque,但其所含对象的排序不是FIFO,而是依据对象的自然排序或者是噶欧早函数的Comparator决定的顺序。
  • SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
 public static void main(String[] args) {
        BlockingQueue<String> queue
            = new LinkedBlockingDeque<String>();
        
        try {
            //queue.offer("A");//立即向队列末尾追加元素
            
            /*
             * 向队列末尾追加元素,指定可延迟5秒。
             * 若5秒钟内成功将元素加入队列返回true
             * 若超时后元素仍然没有加入队列则返回flase
             */
            queue.offer("A",5,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(queue.poll());
    }

  

案例12:线程池演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
 * 线程池主要解决两个问题:
 * 1.控制线程数量
 * 2.重用该线程
 * 由于每个线程都需要资源消耗,且占用CPU计算时间,那么线程数量过大会对系统资源消耗
 * 产生映像,并且由于CPU过度切换导致系统拖慢,频繁的创建线程和销毁线程也会给系统带
 * 来不小的开销,为此重用线程和控制线程数量是在使用线程时必须注意的问题。
 */
public class ThreadPoolDemo {
	public static void main(String[] args) {
		ExecutorService threadPool = Executors.newFixedThreadPool(2);  //创建2个线程的线程池
		for(int i=0;i<5;i++){
			Runnable runn = new Runnable(){
				public void run(){
					Thread t = Thread.currentThread();
					System.out.println(t.getName()+":正在运行任务...");
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(t.getName()+":任务运行完毕!");
				}
			};
			/*
			 * 将任务交给线程池去运行,如果线程池中有空闲线程
			 * 那么会立即分配线程来运行这个任务,若没有,则会
			 * 加入线程池内部的队列,等待线程来运行
			 */
			threadPool.execute(runn);
			System.out.println("指派了一个任务...");
		}
		/*
		 * 如果不加shutdown方法,则任务运行结束后,线程池不会关闭会继续等待下一次任务分配,
		 * 因为线程池是前台进程,所以程序处于一直运行状态。
		 * 
		 * shutdown():当线程池中所有任务运行完毕后停止线程池。
		 * shutdownNow():强制将线程池中所有线程中断,并停止线程池。
		 */
//		threadPool.shutdown();
		threadPool.shutdownNow();
		System.out.println("线程池停止了.");
	}
}
/*
threadPool.shutdown():运行结果:
指派了一个任务...
指派了一个任务...
指派了一个任务...
指派了一个任务...
指派了一个任务...
pool-1-thread-1:正在运行任务...
pool-1-thread-2:正在运行任务...
线程池停止了.
pool-1-thread-1:任务运行完毕!
pool-1-thread-1:正在运行任务...
pool-1-thread-2:任务运行完毕!
pool-1-thread-2:正在运行任务...
pool-1-thread-1:任务运行完毕!
pool-1-thread-1:正在运行任务...
pool-1-thread-2:任务运行完毕!
pool-1-thread-1:任务运行完毕!

threadPool.shutdownNow()运行结果:
指派了一个任务...
指派了一个任务...
指派了一个任务...
指派了一个任务...
pool-1-thread-1:正在运行任务...
pool-1-thread-2:正在运行任务...
指派了一个任务...
线程池停止了.
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at javase.day09.ThreadPoolDemo$1.run(ThreadPoolDemo.java:23)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)
java.lang.InterruptedException: sleep interrupted
pool-1-thread-1:任务运行完毕!
	at java.lang.Thread.sleep(Native Method)
	at javase.day09.ThreadPoolDemo$1.run(ThreadPoolDemo.java:23)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
	at java.lang.Thread.run(Unknown Source)
pool-1-thread-2:任务运行完毕!

*/

 

 

 

以上是关于Java 多线程基础的主要内容,如果未能解决你的问题,请参考以下文章

java基础入门-多线程同步浅析-以银行转账为样例

java多线程基础

Java基础之多线程

Java多线程基础

多线程编程学习一(Java多线程的基础)

Java 多线程基础多线程的实现方式