一、多线程的概述
进程: 在一个操作系统中,每个独立执行的程序都可称为一个进程,也就是“正在运行的程序”。
线程: 每个运行的程序都是一个进程,在一个进程中可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。
二、线程的创建方式
线程创建的方式:
- 第一种:继承Thread并重写Run方法来定义要执行的任务
- 第二种:实现Runnable接口并重写run方法
第一种创建方式的不足点:
1.由于Java是单继承,那么当继承了Thread后,就不能继承其他的类。
2.由于继承Thread后重写run方法规定了线程要执行的任务,这导致线程与任务有一个必然的耦合关系,不利于线程的重用。故经常采用第二种方式。
创建线程的五种写法:
- 第一种写法(继承Thread并重写Run方法)
@Test
public void testThreadDemo(){
Thread t1 = new MyThread1();
Thread t2 = new MyThread2();
t1.start();
t2.start();
}
class MyThread1 extends Thread{
public void run(){
for(int i = 0 ; i<10000;i++){
System.out.println("你是谁啊?");
}
}
}
class MyThread2 extends Thread{
public void run(){
for(int i = 0 ; i <10000;i++){
System.out.println("我是查水表的!");
}
}
}
- 第二种写法(实现Runnable接口并重写run方法)
@Test
public void testThreadDemo1() {
Runnable r1 = new MyRunnable1();
Runnable r2 = new MyRunnable2();
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
class MyRunnable1 implements Runnable{
@Override
public void run() {
for(int i = 0; i<1000;i++){
System.out.println("你是谁啊?");
}
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
for(int i = 0;i<1000;i++){
System.out.println("我是查水表的!");
}
}
- 第三种写法(在写法1的基础上采用匿名内部类的形式)
@Test
public void testThreadDemo2(){//方式一的匿名内部类形式
Thread t1 = new Thread(){
public void run(){
for(int i = 0;i<1000;i++){
System.out.println("你是谁啊?");
}
}
};
Thread t2 = new Thread(){
public void run(){
for(int i = 0;i<1000;i++){
System.out.println("我是查水表的!");
}
}
};
t1.start();
t2.start();
}
- 第四种写法(在写法2的基础上采用匿名内部类的形式)
@Test
public void testThreadDemo3 (){//方式二的匿名内部类形式
Runnable r1 = new Runnable(){
public void run(){
for(int i = 0;i<10000;i++){
System.out.println("你是谁啊?");
}
}
};
Runnable r2 = new Runnable(){
public void run(){
for(int i = 0; i<10000;i++){
System.out.println("我是查水表的");
}
}
};
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
}
- 第五种写法(升级版)
@Test
public void testThreadDemo4(){
new Thread(new Runnable(){
public void run(){
for(int i = 0; i<1000;i++){
System.out.println("你是谁啊?");
}
}}){
}.start();
new Thread(new Runnable(){
public void run(){
for(int i = 0; i<1000;i++){
System.out.println("我是查水表的!");
}
}
}){
}.start();;
}
注意:
启动线程要指定start方法,而不是直接调用run方法,run方法是线程要执行的任务。
当线程的start方法被调用后,线程进入Runnable状态,一旦获取CPU时间,run方法就会被自动调用。
三、线程的生命周期
四、Thread相关API
- long getId():返回该线程的标识符
- String getName():返回该线程的名称
- int getPriority():返回线程的优先级
- Thread.state getState():获取线程的状态
- boolean isAlive():测试线程是否处于活动状态
- boolean isDaenmon():测试线程是否为守护线程
- boolean isInterrupted():测试线程是否已经中断
测试:
@Test
public void testThreadAPIDemo2(){
Thread main = Thread.currentThread();
long id = main.getId();
//long getId():返回该线程的标识符
System.out.println(id);//1
String name = main.getName();
//- String getName():返回该线程的名称
System.out.println(name);//main
int priority = main.getPriority();
//int getPriority():返回线程的优先级
System.out.println(priority);//5
boolean isAlive = main.isAlive();
//boolean isAlive():测试线程是否处于活动状态
System.out.println("isAlive:"+isAlive);//isAlive:true
boolean daemon = main.isDaemon();
//boolean isDaenmon():测试线程是否为守护线程
System.out.println("isDaemon:"+daemon);
boolean isInterrupted = main.isInterrupted();//isDaemon:false
//boolean isInterrupted():测试线程是否已经中断
System.out.println("isInterrupted:"+isInterrupted);//isInterrupted:false
}
- static void sleep(long ms)
- 线程提供的静态方法sleep可以使运行该方法的线程进入阻塞状态指定毫秒。超时后线程会自动回到Runnable状态 等待分配时间片。注意该方法声明抛出一个InterruptException。所以在使用该方法时需要捕获这个异常。
测试代码:
@Test
public void testClock(){
while(true){
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
Date date = new Date();
String str = sdf.format(date);
System.out.println(str);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- Thread的方法join
- void join()
- 该方法用于等待当前线程结束
- 该方法声明抛出InterruptException.
- join方法可以是调用该方法的线程进入阻塞状态,直到该方法所属线程完成工作才会解除调用该方法线程的阻塞状态。 join方法一般用来完成多个线程之间的同步工作问题。
测试代码:
public class ThreadJoinDemo {
//表示图片是否下载完毕
public static boolean isFinish = false;
public static void main(String[] args) {
Thread downLoad = new Thread(){
public void run(){
System.out.println("开始下载图片!");
for(int i = 1;i<=100;i++){
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:开始显示图片!");
/*
* 先等待downLoad将图片下载完毕!
*/
try {
downLoad.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!isFinish){
throw new RuntimeException ("图片没有下载!");
}
System.out.println("show:图片显示完毕。。。");
}
};
downLoad.start();
show.start();
}
}
- static Thread currentThread() 获取运行当前方法的线程
测试代码:
@Test
public void testThreadAPIDemo1(){
Thread main = Thread.currentThread();
System.out.println("运行main方法的线程是:"+main);//运行main方法的线程是:Thread[main,5,main]
doSome();
Thread t = new Thread(){
public void run(){
Thread t = Thread.currentThread();
System.out.println("运行自定义线程的是"+t);//运行自定义线程的是Thread[Thread-0,5,main]
doSome();//运行dosome方法的线程是Thread[Thread-0,5,main]
}
};
t.start();
}
public static void doSome(){
Thread t = Thread.currentThread();
System.out.println("运行dosome方法的线程是"+t);//运行dosome方法的线程是Thread[main,5,main]
}
五、线程的优先级
线程的时间片分配完全是听线程调度的。线程只能被动的被分配时间。对于线程调度的工作不能干预。 但是可以通过提高线程的优先级来达到尽可能干预的目的。理论上,优先级越高的线程,获取CPU时间片的次数就越多。
- 线程的优先级被划分为十个等级,值分别是1-10,其中1最低,10最高。线程提供了3个常量来表示
最低、最高,以及默认优先级:
Thread.MIN_PRIORITY
hread.MAX_PRIORITY
Thread.NORM_PRIORITY
- void setPriority(int priority):该方法用于设置线程的优先级
测试代码:
@Test
public void testSetPriority(){
Thread t1 = new Thread (){
public void run(){
for(int i = 0;i<10000;i++){
System.out.println("min");
}
}
};
Thread t2 = new Thread (){
public void run(){
for(int i = 0;i<10000;i++){
System.out.println("norm");
}
}
};
Thread t3 = new Thread (){
public void run(){
for(int i = 0;i<10000;i++){
System.out.println("max");
}
}
};
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.NORM_PRIORITY);
t3.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
t3.start();
}
六、后台线程
守护线程,又称为后台线程
-
当一个进程中的所有前台线程都结束时,进程就要结束,若还有后台线程运行,那么后台线程会被强制结束。
-
守护线程与普通线程在表现上没有什么区别,我们只需要通过Thread提供的方法来设定即可:
-
void setDaemon(boolean)
-
当参数为true时该线程为守护线程。
-
守护线程的特点是,当进程中只剩下守护线程时,所有守护线程强制终止。
-
GC就是运行在一个守护线程上。
测试代码:
public class Test {
public static void main(String[] args) {
/*
* Rose:前台线程
*/
Thread Rose = new Thread(){
public void run(){
for(int i = 0;i<10;i++){
System.out.println("Rose:Let me go!");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("Rose: 啊啊啊啊啊啊AAAAAAaaaaaa..........");
System.out.println("音效: 噗通.........");
}
};
/*
* Jack: 后台线程
*/
Thread Jack = new Thread(){
public void run(){
while(true){
System.out.println("Jack: You jump I jump!!!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
};
//将Jack设置为后台线程,并在start启动前调用
Jack.setDaemon(true);
Rose.start();
Jack.start();
System.out.println("main方法结束了");
}
}
小结一下:经过测试可知,main方法只执行一次,main方法执行完毕后程序并没有立即停止。原因是,程序中还存在前台线程和后台线程,所以还得继续将前台线程执行完毕,当程序中只剩后台线程时,进程就结束了,JVM也就退出了。
七、synchronized关键字
多线程并发访问同一资源时,就会形成“抢”的现象。由于线程切换时机不确定,可能导致执行代码顺序的混乱,严重时会导致系统瘫痪。为了解决这一问题可以使用synchronized关键字。
当一个方法被synchronized修饰后,该方法为同步方法,即:多个线程不能同时进入方法内部执行。对于成员方法而言,synchronized会在一个线程调用该方法是将该方法所属对象加锁其他线程在执行该方法 时由于执行方法的线程没有释放锁,所以只能在方法外阻塞,知道持有方法的锁的线程将方法执行完毕。 所以,解决多线程并发执行安全问题的方法就是将“抢”变为“排队”
7.1 synchronized关键字修饰方法:
测试代码:
/*
*模拟两个人从桌子上拿豆子(排队拿,一个拿完一个拿)
*/
public class ThreadDemo1 {
public static void main(String[] args) {
Table table = new Table();
Thread t1 = new Thread(){
public void run(){
while(true){
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 synchronized int getBean(){
if(beans==0){
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}
7.2 同步块
同步块可以有效的缩小同步范围可以在保证并发安全的同时尽可能的提高并发效率。
同步块可以要求多个线程对该块内的代码排队执行,但是前提条件是 同步监视器对象(即上锁的对象)要求多个线程看到的必须是同一个。
* synchronized (同步监视器对象){
* 需要同步的代码
* }
所谓同步执行即:多个线程必须排队执行
所谓异步执行即:多个线程可以同时执行
测试代码:
/*
* 模拟一个试衣间,挑衣服可以同步进行,试衣这个环节的代码块被synchronized关键字修饰,
* 所以在在这个同步块中只能排队执行,一个试完一个试。
*/
public class SyncDemo {
public static void main(String[] args) {
// Shop shop1 = new Shop();
// Shop shop2 = new Shop();
Shop shop = new Shop();
Thread t2 = new Thread(){
public void run(){
shop.buy();
}
};
Thread t3 = new Thread(){
public void run(){
shop.buy();
}
};
t2.start();
t3.start();
}
}
class Shop{
public void buy(){
Thread t = Thread.currentThread();//获取运行buy方法的线程
try {
System.out.println(t.getName()+":正在挑衣服。。。");
Thread.sleep(5000);
synchronized (this) {
System.out.println(t.getName()+":正在试衣服。。。");
Thread.sleep(5000);
}
System.out.println(t.getName()+"结账离开!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.3 静态方法的同步
当一个静态方法被synchronized修饰后,那么该方法即为同步方法,由于静态方法从属类,全局就一份,所以同步的静态方法一定具有同步效果,与对象无关。
测试代码:
public class SyncDemo2 {
public static void main(String[] args) {
Foo f1 = new Foo();
Foo f2 = new Foo();
Thread t1 = new Thread(){
public void run(){
f1.dosome();//与对象无关
}
};
Thread t2 = new Thread(){
public void run(){
f2.dosome();//与对象无关
}
};
t1.start();
t2.start();
}
}
class Foo{
public synchronized static void dosome(){
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+":正在运行。。。");
Thread.sleep(1000);
System.out.println(t.getName()+":执行完毕!!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
7.4 互斥锁
Synchronized也叫互斥锁,即: 使用synchronized修饰多段代码,只要他们的同步监视器对象相同,那么这几段代码间就是互斥关系,即多个线程不能同时执行这些代码。
测试代码:
public class SyncDemo3 {
public static void main(String[] args) {
Boo b1 = new Boo();
//Boo b2 = new Boo();
Thread t1 = new Thread(){
public void run(){
b1.methodA();
}
};
Thread t2 = new Thread(){
public void run(){
b1.methodB();
}
};
t1.start();
t2.start();
}
}
class Boo{
public synchronized void methodA(){
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+"正在执行。。。");
Thread.sleep(5000);
System.out.println(t.getName()+"执行完毕。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void methodB(){
try {
Thread t = Thread.currentThread();
System.out.println(t.getName()+"正在执行。。。");
Thread.sleep(5000);
System.out.println(t.getName()+"执行完毕。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:谁先拿到cpu时间片先执行哪个线程,又线程1和线程2是互斥关系,所以一个执行完了才会去执行另一个。
八、线程安全API和非线程安全API
获取线程安全的集合方式:
- Collections.synchronizedList() 获取线程安全的List集合
- Collections.synchronizedMap() 获取线程安全的Map
测试代码:
@Test
public void testSyncDemo(){
/*
* ArrayList和LinkedList都不是线程安全的
*/
List<String> list = new ArrayList<String>();
list.add("one");
list.add("two");
list.add("three");
list.add("four");
System.out.println(list);
/*
* 将给定集合list转换为线程安全的集合
*/
list = Collections.synchronizedList(list);
System.out.println(list);
/*
* HashSet不是线程安全的
*/
Set<String> set = new HashSet<String>(list);
System.out.println(set);
/*
* 将给定的集合set转换为线程安全的集合
*/
set = Collections.synchronizedSet(set);
System.out.println(set);
/*
* HashMap也不是线程安全的
*/
Map<String,Integer> map = new HashMap<>();
map.put("语文", 100);
map.put("数学", 99);
map.put("英语", 89);
map.put("政治", 79);
System.out.println(map);
/*
* 将给定的集合map转化为线程安全的
*/
map = Collections.synchronizedMap(map);
System.out.println(map);
/*
* API手册上有说明
* 就算是线程安全的集合那么其中对于元素的操作,如add,remove等方法都不与迭代器遍历做互斥,
* 需要自行维护互斥关系。
*/
}
九、线程池
/*
* 使用ExecutorService实现线程池
* ExecutorService是Java提供的用于管理线程池的类。
* 线程池主要有两个作用:
* - 控制线程数量
* - 重用线程
* 当一个程序中若创建大量线程,并存在任务结束后销毁,会给系统带来过度消耗资源,
* 以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
*
* 线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中
* 取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程中。
*
* 在线程池的编辑模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部
* 找有无空闲的线程,再把任务交给内部某个空闲的线程。
*
* 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
*
* 线程池有以下几种实现策略:
* - Executors.newCachedThreadPool()
* 创建一个可根据需要创建线程的线程池,但是在以前构造的线程可用时将重用它们。
* - Executors.newFixedThreadPool(int nThreads)
* 创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
* - Executors.newScheduledThreadPool(int corePoolSize)
* 创建一个线程池,它可安排在给定延迟后运行命令或者定期的执行。
* - Executors.newSingleThreadExecutor()
* 创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程。
*/
public class ExceutorServiceDemo {
/*
* 线程池
* 线程池的主要两个作用:
* 1.重用线程
* 2.控制线程数量
* 当我们的应用需要创建大量线程或者发现线程会频繁的创建和销毁时就应该考虑使用线程池来维护线程。
*/
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for(int i = 0; i <5;i++){
Runnable runn = new Runnable(){
public void run(){
Thread t = Thread.currentThread();
try {
System.out.println(t+"正在运行任务");
Thread.sleep(5000);
System.out.println(t+"任务运行结束");
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
}
};
threadPool.execute(runn);
System.out.println("指派了一个任务交给线程池");
}
threadPool.shutdown();//将线程池内的工作干完才会停掉线程池
//threadPool.shutdownNow();//表示立刻停掉线程池,无论线程池中是否还存在未执行完的任务
System.out.println("停止线程池了");
}