java多线程(超详细)
Posted Mr.乐.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程(超详细)相关的知识,希望对你有一定的参考价值。
1 - 线程
1.1 - 进程
进程就是正在运行中的程序(进程是驻留在内存中的)
-
是系统执行资源分配和调度的独立单位
-
每一进程都有属于自己的存储空间和系统资源
-
注意:进程A和进程B的内存独立不共享。
1.2 - 线程
线程就是进程中的单个顺序控制流,也可以理解成是一条执行路径
-
单线程:一个进程中包含一个顺序控制流(一条执行路径)
-
多线程:一个进程中包含多个顺序控制流(多条执行路径)
-
在java语言中:
线程A和线程B,堆内存和方法区内存共享。
但是栈内存独立,一个线程一个栈。 -
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
-
java中之所以有多线程机制,目的就是为了提高程序的处理效率。
-
对于单核的CPU来说,不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是多个事情同时在做。
1.3 -java中多线程的生命周期
就绪状态:就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺CPU时间片的权力(CPU时间片就是执行权)。当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
运行状态:run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态继续抢夺CPU时间片,当再次抢到CPU时间之后,会重新进入run方法接着上一次的代码继续往下执行。
阻塞状态:当一个线程遇到阻塞事件,例如接收用户键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的CPU时间片。之前的时间片没了需要再次回到就绪状态抢夺CPU时间片。
锁池:在这里找共享对象的对象锁线程进入锁池找共享对象的对象锁的时候,会释放之前占有CPU时间片,有可能找到了,有可能没找到,没找到则在锁池中等待,如果找到了会进入就绪状态继续抢夺CPU时间片。(这个进入锁池,可以理解为一种阻塞状态)
1.4 - 多线程的实现方式(一)
-
继承Thread类
1、自定义一个类MyThread类,用来继承与Thread类
2、在MyThread类中重写run()方法
3、在测试类中创建MyThread类的对象
4、启动线程
/**
* @author Mr.乐
* @Description
*/
public class Demo01
public static void main(String[] args)
//创建线程
MyThread t01 = new MyThread();
MyThread t02 = new MyThread();
MyThread t03 = new MyThread("线程03");
//开启线程
// t01.run();
// t02.run();
// t03.run();
// 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
// start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
// 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
// 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
// run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
t01.start();
t02.start();
t03.start();
//设置线程名(补救的设置线程名的方式)
t01.setName("线程01");
t02.setName("线程02");
//设置主线程名称
Thread.currentThread().setName("主线程");
for (int i = 0; i < 50; i++)
//Thread.currentThread() 获取当前正在执行线程的对象
System.out.println(Thread.currentThread().getName() + ":" + i);
class MyThread extends Thread
public MyThread()
public MyThread(String name)
super(name);
//run方法是每个线程运行过程中都必须执行的方法
@Override
public void run()
for (int i = 0; i < 50; i++)
System.out.println(this.getName() + ":" + i);
此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。
start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
单纯使用run()方法是不能多线程并发的。
1.5 - 设置和获取线程名
-
设置线程名
-
setName(String name)
:设置线程名 -
通过带参构造方法设置线程名
-
-
获取线程名
-
getName()
:返回字符串形式的线程名 -
Thread.CurrentThread():
返回当前正在执行的线程对象
-
1.6 - 多线程的实现方式(二)
-
实现Runnable接口
1、自定义一个MyRunnable类来实现Runnable接口
2、在MyRunnable类中重写run()方法
3、创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去
4、启动线程
/**
* @author Mr.乐
* @Description
*/
public class Demo02
public static void main(String[] args)
MyRunnable myRun = new MyRunnable();//将一个任务提取出来,让多个线程共同去执行
//封装线程对象
Thread t01 = new Thread(myRun, "线程01");
Thread t02 = new Thread(myRun, "线程02");
Thread t03 = new Thread(myRun, "线程03");
//开启线程
t01.start();
t02.start();
t03.start();
//通过匿名内部类的方式创建线程
new Thread(new Runnable()
@Override
public void run()
for (int i = 0; i < 20; i++)
System.out.println(Thread.currentThread().getName() + " - " + i);
,"线程04").start();
//自定义线程类,实现Runnable接口
//这并不是一个线程类,是一个可运行的类,它还不是一个线程。
class MyRunnable implements Runnable
@Override
public void run()
for (int i = 0; i < 50; i++)
System.out.println(Thread.currentThread().getName() + " - " + i);
1.7 - 多线程的实现方式(三)
实现Callable接口( java.util.concurrent.FutureTask; /JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。)
1、自定义一个MyCallable类来实现Callable接口
2、在MyCallable类中重写call()方法
3、创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。
4、启动线程
这种方式的优点:可以获取到线程的执行结果。
这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/**
* @author Mr.乐
* @Description 线程实现的第三种方式
*/
public class Demo04
public static void main(String[] args) throws Exception
// 第一步:创建一个“未来任务类”对象。
// 参数非常重要,需要给一个Callable接口实现类对象。
FutureTask task = new FutureTask(new Callable()
@Override
public Object call() throws Exception // call()方法就相当于run方法。只不过这个有返回值
// 线程执行一个任务,执行之后可能会有一个执行结果
// 模拟执行
System.out.println("call method begin");
Thread.sleep(1000 * 10);
System.out.println("call method end!");
int a = 100;
int b = 200;
return a + b; //自动装箱(300结果变成Integer)
);
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
// 这里是main方法,这是在主线程中。
// 在主线程中,怎么获取t线程的返回结果?
// get()方法的执行会导致“当前线程阻塞”
Object obj = task.get();
System.out.println("线程执行结果:" + obj);
// main方法这里的程序要想执行必须等待get()方法的结束
// 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
// 另一个线程执行是需要时间的。
System.out.println("hello world!");
1.8 -线程控制
方法名 | 说明 |
---|---|
void yield() | 使当前线程让步,重新回到争夺CPU执行权的队列中 |
static void sleep(long ms) | 使当前正在执行的线程停留指定的毫秒数 |
void join() | 等死(等待当前线程销毁后,再继续执行其它的线程) |
void interrupt() | 终止线程睡眠 |
1.8.1 -sleep()方法 (谁执行谁就是当前线程)
/**
* @author Mr.乐
* @Description 线程睡眠
*/
public class DemoSleep
public static void main(String[] args)
// 创建线程
MyThread1 t01 = new MyThread1("黄固");
MyThread1 t02 = new MyThread1("欧阳锋");
MyThread1 t03 = new MyThread1("段智兴");
MyThread1 t04 = new MyThread1("洪七公");
//开启线程
t01.start();
t02.start();
t03.start();
t04.start();
class MyThread1 extends Thread
public MyThread1()
public MyThread1(String name)
super(name);
@Override
// 重点:run()当中的异常不能throws,只能try catch
// 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
public void run()
for (int i = 1; i < 50; i++)
System.out.println(this.getName() + "正在打出第 - " + i + "招");
try
Thread.sleep(500);//让当前正在执行的线程睡眠指定毫秒数
catch (InterruptedException e)
e.printStackTrace();
注意:run()方法中的异常只能try catch,因为父类没有抛出异常,子类不能抛出比父类更多的异常。
1.8.2 -interrupt()方法和stop()方法
/**
* @author Mr.乐
* @Description 终止线程
*/
public class DemoInterrupt
public static void main(String[] args)
Thread t = new Thread(new MyRunnable2());
t.setName("t");
t.start();
try
Thread.sleep(1000 * 5);
catch (InterruptedException e)
e.printStackTrace();
// 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
t.interrupt();
// t.stop(); //强行终止线程
//缺点:容易损坏数据 线程没有保存的数据容易丢失
class MyRunnable2 implements Runnable
@Override
public void run()
System.out.println(Thread.currentThread().getName() + "---> begin");
try
// 睡眠1年
Thread.sleep(1000 * 60 * 60 * 24 * 365);
catch (InterruptedException e)
// e.printStackTrace();
//1年之后才会执行这里
System.out.println(Thread.currentThread().getName() + "---> end");
1.8.3 -合理的终止线程
做一个boolean类型的标记
/**
* @author Mr.乐
* @Description
*/
public class DemoSleep02
public static void main(String[] args)
MyRunable4 r = new MyRunable4();
Thread t = new Thread(r);
t.setName("t");
t.start();
// 模拟5秒
try
Thread.sleep(5000);
catch (InterruptedException e)
e.printStackTrace();
// 终止线程
// 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
r.run = false;
class MyRunable4 implements Runnable
// 打一个布尔标记
boolean run = true;
@Override
public void run()
for (int i = 0; i < 10; i++)
if(run)
System.out.println(Thread.currentThread().getName() + "--->" + i);
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
else
// return就结束了,你在结束之前还有什么没保存的。
// 在这里可以保存呀。
//save....
//终止当前线程
return;
1.8.4 - yield()
暂停当前正在执行的线程对象,并执行其他线程
yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。
yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。
注意:在回到就绪之后,有可能还会再次抢到。
/**
* @author Mr.乐
* @Description 线程让位
*/
public class DemoYield
public static void main(String[] args)
//创建线程
MyThread5 t01 = new MyThread5("线程01");
MyThread5 t02 = new MyThread5("线程02");
MyThread5 t03 = new MyThread5("线程03");
//开启线程
t01.start();
t02.start();
t03.start();
class MyThread5 extends Thread
public MyThread5()
public MyThread5(String name)
super(name);
@Override
public void run()
for (int i = 0; i < 50; i++)
if(30 == i)
Thread.yield();//当循i环到30时,让线程让步
//1、回到抢占队列中,又争夺到了执行权
//2、回到抢占队列中,没有争夺到执行权
System.out.println(this.getName() + ":" + i);
1.8.5 -join()
1.9 - 线程的调度
-
线程调度模型
-
均分式调度模型:所有的线程轮流使用CPU的使用权,平均分配给每一个线程占用CPU的时间。
-
抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么就会随机选择一个线程来执行,优先级高的占用CPU时间相对来说会高一点点。
Java中JVM使用的就是抢占式调度模型
-
-
getPriority()
:获取线程优先级 -
setPriority
:设置线程优先级
/**
* @author Mr.乐
* @Description 线程的调度
*/
public class Demo07
public static void main(String[] args)
//创建线程
MyThread t01 = new MyThread("线程01");
MyThread t02 = new MyThread("线程02");
MyThread t03 = new MyThread("线程03");
//获取线程优先级,默认是5
// System.out.println(t01.getPriority());
// System.out.println(t02.getPriority());
// System.out.println(t03.getPriority());
//设置线程优先级
t01.setPriority(Thread.MIN_PRIORITY); //低 - 理论上来讲,最后完成
t02.setPriority(Thread.NORM_PRIORITY); //中
t03.setPriority(Thread.MAX_PRIORITY); //高 - 理论上来讲,最先完成
//开启线程
t01.start();
t02.start();
t03.start();
2 - 线程的安全
2.1 - 数据安全问题
-
是否具备多线程的环境
-
是否有共享数据
-
是否有多条语句操作共享数据
-
例如:我和小明同时取一个账户的钱,我取钱后数据还没返回给服务器,小明又取了,这个时候小明的余额还是原来的。
-
如何解决?线程排队执行(不能并发),线程同步机制。
2.1.1 -变量对线程安全的影响
实例变量:在堆中。
静态变量:在方法区。
局部变量:在栈中。
以上三大变量中:
局部变量永远都不会存在线程安全问题。
因为局部变量不共享。(一个线程一个栈。)
局部变量在栈中。所以局部变量永远都不会共享。
实例变量在堆中,堆只有1个。
静态变量在方法区中,方法区只有1个。
堆和方法区都是多线程共享的,所以可能存在线程安全问题。
局部变量+常量:不会有线程安全问题。
成员变量:可能会有线程安全问题。
2.1.2 -模拟线程安全问题
public class Test
public static void main(String[] args)
// 创建账户对象(只创建1个)
Account act = new Account("act-001", 10000);
// 创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
// 设置name
t1.setName("t1");
t2.setName("t2");
// 启动线程取款
t1.start();
t2.start();
//t1对act-001取款5000.0成功,余额5000.0
//t2对act-001取款5000.0成功,余额5000.0
----------------------------------------------------
public class AccountThread extends Thread
// 两个线程必须共享同一个账户对象。
private Account act;
// 通过构造方法传递过来账户对象
public AccountThread(Account act)
this.act = act;
public void run()
// run方法的执行表示取款操作。
// 假设取款5000
double money = 5000;
// 取款
// 多线程并发执行这个方法。
act.withdraw(money);
System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
------------------------------------------------
/**
* @author Mr.乐
* @Description
*/
public class Account
// 账号
private String actno;
// 余额
private double balance;
public Account()
public Account(String actno, double balance)
this.actno = actno;
this.balance = balance;
public String getActno()
return actno;
public void setActno(String actno)
this.actno = actno;
public double getBalance()
return balance;
public void setBalance(double balance)
this.balance = balance;
//取款的方法
public void withdraw(double money)
// t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
// 取款之前的余额
double before = this.getBalance(); // 10000
// 取款之后的余额
double after = before - money;
// 在这里模拟一下网络延迟,100%会出现问题
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
// 更新余额
// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
this.setBalance(after);
2.2 - 线程同步的利弊
-
好处:解决了线程同步的数据安全问题
-
弊端:当线程很多的时候,每个线程都会去判断同步上面的这个锁,很耗费资源,降低效率
2.3 -编程模型
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
谁也不需要等谁,这种编程模型叫做:异步编程模型。
其实就是:多线程并发(效率较高。)
同步编程模型:
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
两个线程之间发生了等待关系,这就是同步编程模型。
效率较低。线程排队执行。
2.4 -线程同步
2.4.1 -线程同步方式
同步语句块:synchronized(this)方法体 (synchronized括号后的数据必须是多线程共享的数据,才能达到多线程排队)
// 以下代码的执行原理?
// 1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
// 2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
// 找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
// 占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
// 3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
// 共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
// 直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
// t2占有这把锁之后,进入同步代码块执行程序。
//
// 这样就达到了线程排队执行。
// 这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
// 执行的这些线程对象所共享的。
synchronized (this)
double before = this.getBalance();
double after = before - money;
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
this.setBalance(after);
普通同步方法:修饰符 synchronized 返回值类型 方法名(形参列表)方法体
synchronized出现在实例方法上,一定锁的是this(此方法)。不能是其他的对象了。 所以这种方式不灵活。
另外还有一个缺点:synchronized出现在实例方法上, 表示整个方法体都需要同步,可能会无故扩大同步的 范围,导致程序的执行效率降低。所以这种方式不常用。
public synchronized void withdraw(double money)
double before = this.getBalance(); // 10000
// 取款之后的余额
double after = before - money;
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
// 更新余额
this.setBalance(after);
静态同步方法:修饰符 synchronized static 返回值类型 方法名(形参列表)方法体
(静态方法中不能使用this)表示找类锁。类锁永远只有1把。
2.5 -如何解决线程安全问题
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
线程同步机制。
第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
就只能选择synchronized了。线程同步机制。
2.6 -Lock
应用场景不同,不一定要在同一个方法中进行解锁,如果在当前的方法体内部没有满足解锁需求时,可以将lock引用传递到下一个方法中,当满足解锁需求时进行解锁操作,方法比较灵活。
private Lock lock = new ReentrantLock();//定义Lock类型的锁
public void withdraw(double money)
// t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
// 取款之前的余额
lock.lock();//上锁
double before = this.getBalance(); // 10000
// 取款之后的余额
double after = before - money;
// 在这里模拟一下网络延迟,100%会出现问题
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
// 更新余额
// 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
this.setBalance(after);
lock.unlock();//解锁
2.7 -死锁
形成原因
当两个线程或者多个线程互相锁定的情况就叫死锁
避免死锁的原则
顺序上锁,反向解锁,不要回头
/**
* @author Mr.乐
* @Description 死锁
*/
public class DeadLock
public static void main(String[] args)
Object o1 = new Object();
Object o2 = new Object();
// t1和t2两个线程共享o1,o2
Thread t1 = new MyThread1(o1,o2);
Thread t2 = new MyThread2(o1,o2);
t1.start();
t2.start();
class MyThread1 extends Thread
Object o1;
Object o2;
public MyThread1(Object o1,Object o2)
this.o1 = o1;
this.o2 = o2;
public void run()
synchronized (o1)
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
synchronized (o2)
class MyThread2 extends Thread
Object o1;
Object o2;
public MyThread2(Object o1,Object o2)
this.o1 = o1;
this.o2 = o2;
public void run()
synchronized (o2)
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
synchronized (o1)
2.8 -守护线程
java语言中线程分为两大类:
一类是:用户线程
一类是:守护线程(后台线程)
其中具有代表性的就是:垃圾回收线程(守护线程)。
守护线程的特点:
一般守护线程是一个死循环,所有的用户线程只要结束,
守护线程自动结束。
注意:主线程main方法是一个用户线程。
守护线程用在什么地方呢?
每天00:00的时候系统数据自动备份。
这个需要使用到定时器,并且我们可以将定时器设置为守护线程。
一直在那里看着,每到00:00的时候就备份一次。所有的用户线程
如果结束了,守护线程自动退出,没有必要进行数据备份了。
public class Demo09
public static void main(String[] args)
Thread t = new BakDataThread();
t.setName("备份数据的线程");
// 启动线程之前,将线程设置为守护线程
t.setDaemon(true);
t.start();
// 主线程:主线程是用户线程
for(int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "--->" + i);
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
class BakDataThread extends Thread
public void run()
int i = 0;
// 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
while(true)
System.out.println(Thread.currentThread().getName() + "--->" + (++i));
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
3 -定时器
定时器的作用:
间隔特定的时间,执行特定的程序。
在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。
不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持
定时任务的。
import java.util.Timer;
import java.util.TimerTask;
/**
* @author Mr.乐
* @Description 定时类
*/
public class DemoTimer
public static void main(String[] args)
Timer timer = new Timer();//创建Timer定时器类的对象
//匿名内部类
timer.schedule(new TimerTask()
@Override
public void run()
System.out.println("我被执行了!~");
System.gc();//告诉JVM运行完毕,可以把我回收
,5000);
3.1 -线程与定时器执行轨迹不同
线程与定时器之间互不抢占CPU时间片
import java.util.Timer;
import java.util.TimerTask;
/**
* @author Mr.乐
* @Description 线程与定时器的执行轨迹不同
*/
public class DemoTimer
public static void main(String[] args)
new Thread(new Runnable()
@Override
public void run()
for (int i = 0; i < 20; i++)
System.out.println(Thread.currentThread().getName() + "<--->" + i);
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
).start();
//定时器实现
new Timer().schedule(new TimerTask()
@Override
public void run()
for (int i = 0; i < 10; i++)
System.out.println(Thread.currentThread().getName() + "---" + i);
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
System.gc();//将编程垃圾的定时器进行回收
,5000);
4 -生产者和消费者
4.1 -关于Object类中的wait和notify方法。
第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象
都有的方法,因为这两个方式是Object类中自带的。
wait方法和notify方法不是通过线程对象调用,
不是这样的:t.wait(),也不是这样的:t.notify()..不对。
第二:wait()方法作用:
Object o = new Object();
o.wait();表示:
让正在o对象上活动的线程进入等待状态,无期限等待,
直到被唤醒为止。
o.wait();方法的调用,会让“当前线程(正在o对象上
活动的线程)”进入等待状态。
第三:notify()方法作用:
Object o = new Object();
o.notify();表示:
唤醒正在o对象上等待的线程。
还有一个notifyAll()方法:
这个方法是唤醒o对象上处于等待的所有线程。
注意:wait方法和notify方法需要建立在synchronized线程同步的基础之上。
重点:o.wait()方法会让正在o对象上活动的当前线程进入等待状态,并且释放之前占有的o对象的锁;
o.notify()方法只会通知,不会释放之前占有的o对象的锁。
4.2 -生产者和消费者模式
生产者与消费者模式是并发、多线程编程中经典的设计模式,通过wait和notifyAll方法实现。
例如:生产满了,就不能继续生产了,必须让消费线程进行消费。
消费完了,就不能继续消费了,必须让生产线程进行生产。
而消费和生产者共享的仓库,就为多线程共享的了,所以需要考虑仓库的线程安全问题。
wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。
wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
例1:
/**
* @author Mr.乐
* @Description 生产者和消费者模式
*/
public class wait_notify
public static void main(String[] args)
Box box = new Box();//实例化奶箱类
Producer producer = new Producer(box);//生产者对象
Customer customer = new Customer(box);//消费者对象
Thread tp = new Thread(producer);//创建生产者线程
Thread tc = new Thread(customer);//创建消费者线程
//启动线程
tp.start();
tc.start();
//奶箱类
class Box
private int milk; //放入奶箱中的第几瓶牛奶
private boolean state = false; //默认奶箱为空
/**
* 生产者生产(放)牛奶
* @param milk 第几瓶
*/
public synchronized void put(int milk)
if(state) //true表示奶箱中有牛奶
try
wait(); //等待,需要有人唤醒
catch (InterruptedException e)
e.printStackTrace();
//没有牛奶,需要生产牛奶
this.milk = milk;
System.out.println("王五将第" + this.milk + "瓶你牛奶放进了奶箱中");
this.state = true;//将奶箱状态调整成有牛奶
notifyAll();//唤醒全部正在等待的线程
/**
* 消费者取牛奶
*/
public synchronized void get()
if(!state) //true表示奶箱中有牛奶
try
wait(); //等待,需要有人唤醒
catch (InterruptedException e)
e.printStackTrace();
//有牛奶,需要取牛奶
System.out.println("张三将第" + this.milk + "瓶牛奶拿走补了身体!");
this.state = false;//将奶箱状态改变成空
notifyAll();//唤醒全部正在等待的线程
//生产者类
class Producer implements Runnable
private Box b;
public Producer(Box b)
this.b = b;
@Override
public void run()
for (int i = 1; i < 8; i++)
b.put(i);//放牛奶,放几瓶
//消费者类
class Customer implements Runnable
private Box b;
public Customer(Box b)
this.b = b;
@Override
public void run()
while (true)
b.get();//消费者取牛奶
例2:
import java.util.ArrayList;
import java.util.List;
/**
* @author Mr.乐
* @Description 生产者和消费者模式02
*/
public class ThreadTest16
public static void main(String[] args)
// 创建1个仓库对象,共享的。
List list = new ArrayList();
// 创建两个线程对象
// 生产者线程
Thread t1 = new Thread(new Producer(list));
// 消费者线程
Thread t2 = new Thread(new Consumer(list));
t1.setName("生产者线程");
t2.setName("消费者线程");
t1.start();
t2.start();
// 生产线程
class Producer implements Runnable
// 仓库
private List list;
public Producer(List list)
this.list = list;
@Override
public void run()
// 一直生产(使用死循环来模拟一直生产)
while(true)
// 给仓库对象list加锁。
synchronized (list)
if(list.size() > 0) // 大于0,说明仓库中已经有1个元素了。
try
// 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
list.wait();
catch (InterruptedException e)
e.printStackTrace();
// 程序能够执行到这里说明仓库是空的,可以生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒消费者进行消费
list.notifyAll();
// 消费线程
class Consumer implements Runnable
// 仓库
private List list;
public Consumer(List list)
this.list = list;
@Override
public void run()
// 一直消费
while(true)
synchronized (list)
if(list.size() == 0)
try
// 仓库已经空了。
// 消费者线程等待,释放掉list集合的锁
list.wait();
catch (InterruptedException e)
e.printStackTrace();
// 程序能够执行到此处说明仓库中有数据,进行消费。
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "--->" + obj);
// 唤醒生产者生产。
list.notifyAll();
5 -线程池
5.1 - 概念
线程池就是首先创建一些线程,他们的集合称之为线程池。线程池在系统启动时会创建大量空闲线程,程序将一个任务传递给线程池,线程池就会启动一条线程来执行这个任务,执行结束后线程不会销毁(死亡),而是再次返回到线程池中成为空闲状态,等待执行下一个任务。
5.2 - 线程池的工作机制
在线程池的编程模式下,任务是分配给整个线程池的,而不是直接提交给某个线程,线程池拿到任务后,就会在内部寻找是否有空闲的线程,如果有,则将任务交个某个空闲线程。
5.3 - 使用线程池的原因
多线程运行时,系统不断创建和销毁新的线程,成本非常高,会过度的消耗系统资源,从而可能导致系统资源崩溃,使用线程池就是最好的选择。
5.4 - 可重用线程
方法名 | 说明 |
---|---|
Executors.newCacheThreadPoll(); | 创建一个可缓存的线程池 |
execute(Runnable run) | 启动线程池中的线程 |
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Mr.乐
* @Description 可重用线程池
*/
public class ExecutorsTest
public static void main(String[] args)
//创建线程池
ExecutorService threadPoll = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++)
//如果不睡眠,那么第一个执行完的线程无法及时成为空闲线程,那么线程池就会让一个新的线程执行
try
Thread.sleep(1000);
catch (InterruptedException e)
e.printStackTrace();
//每次循环都会开启一个线程
threadPoll.execute(new Runnable()
@Override
public void run()
System.out.println(Thread.currentThread().getName() + "正在被执行!~");
);
threadPoll.shutdown();//关闭线程池
//线程池是无限大,当执行当前任务时,上一个任务已经完成,会重复执行上一个任务的线程,而不是每次使用新的线程
6 -多线程并发的线程安全问题
了解了线程池,接下来从底层讲一下多线程并发的安全问题。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Mr.乐
* @Description 并发安全
*/
public class MyTest
//定义静态变量
static int a=0;
static int count=2000;
public static void main(String[] args)
//创建线程池
ExecutorService service = Executors.newCachedThreadPool();
for(int i=0;i<count;i++)
service.execute(new Runnable()
@Override
public void run()
a++;
);
关闭线程池
service.shutdown();
System.out.println(a);
//1987
以上程序运行并没有达到预期的2000,此处多线程并发,a共享,所以没达到2000
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Mr.乐
* @Description 并发安全
*/
public class MyTest
static int a=0;
static int count=2000;
public static void main(String[] args) throws InterruptedException
ExecutorService service = Executors.newCachedThreadPool();
//闭锁 在一些条件下可放开 参数:加多少把锁
CountDownLatch countDownLatch=new CountDownLatch(count);
for(int i=0;i<count;i++)
service.execute(new Runnable()
@Override
public void run()
a++;
//解一把锁
countDownLatch.countDown();
);
service.shutdown();
//会进入阻塞状态 什么时候把锁全解了 阻塞状态才会解除
countDownLatch.await();
System.out.println(a);
//1987
此处所用的加锁方法也没有实现预期效果。
6.1 -CPU多级缓存
打开任务管理器,在性能中可查看CPU的多级缓存。
程序进程中的数据,都在内存中存着。 而CPU缓存,是为了解决内存没有CPU快的问题。当一个数据需要CPU修改,而内存无法及时给CPU返回数据,就会拖慢CPU的运行速度。所以有了CPU缓存。
当CPU需要在内存中读数据时,在时间局部性上(不久的将来)还得读此数据。,将此数据放在CPU缓存中。
当用到内存中数据(例如 a)时,而数据旁边的数据(例:static int a=0; int b=0; 用a时b为旁边的数据)在空间局部性上,会用到相邻的数据(例如 b),CPU也会读到b,将b数据放在CPU缓存中。
当CPU读取数据时,会让CPU缓存同步内存中的数据。然后CPU缓存中的数据再交给CPU去修改。当CPU修改完后,会把修改的数据传给CPU缓存(此时CPU不需要等待),再由CPU缓存传给内存 。
当CPU 01将数据修改完后,CPU缓存01还没有将数据传给内存,CPU缓存02读到了a,此时a的值为0。
以下为线程安全的两种方式。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Mr.乐
* @Description 并发安全 synchronized
*/
public class MyTest
static int a=0;
static int count=2000;
public static void main(String[] args) throws InterruptedException
ExecutorService service = Executors.newCachedThreadPool();
//闭锁 在一些条件下可放开 参数:加多少把锁
CountDownLatch countDownLatch=new CountDownLatch(count);
for(int i=0;i<count;i++)
service.execute(new Runnable()
@Override
public void run()
synchronized (MyTest.class)
a++;
//解一把锁
countDownLatch.countDown();
);
service.shutdown();
//会进入阻塞状态 什么时候把锁全解了 阻塞状态才会解除
countDownLatch.await();
System.out.println(a);
//2000
-------------------------------------------------------------------
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* @author Mr.乐
* @Description 并发安全 synchronized
*/
public class MyTest
static int a=0;
static int count=2000;
public static void main(String[] args) throws InterruptedException
ExecutorService service = Executors.newCachedThreadPool();
//闭锁 在一些条件下可放开 参数:加多少把锁
CountDownLatch countDownLatch=new CountDownLatch(count);
//信号量
Semaphore semaphore=new Semaphore(1);
for(int i=0;i<count;i++)
service.execute(new Runnable()
@Override
public void run()
try //拿走一个信号
semaphore.acquire();
a++;
//解一把锁
countDownLatch.countDown();
catch (InterruptedException e)
e.printStackTrace();
finally
//释放信号
semaphore.release();
);
service.shutdown();
//会进入阻塞状态 什么时候把锁全解了 阻塞状态才会解除
countDownLatch.await();
System.out.println(a);
//2000
7 -总结
以上就是我对多线程初级的所有总结,希望对大家有所帮助。
Java多线程学习(吐血超详细总结)
- 一扩展javalangThread类
- 二实现javalangRunnable接口
- 三Thread和Runnable的区别
- 四线程状态转换
- 五线程调度
- 六常用函数说明
- 七常见线程名词解释
- 八线程同步
- 九线程数据传递
本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。
首先讲一下进程和线程的区别:
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一程序中有多个顺序流在执行。
在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口。
一、扩展java.lang.Thread类
package com.multithread.learning; /** *@functon 多线程学习 *@author 林炳文 *@time 2015.3.9 */ class Thread1 extends Thread{ private String name; public Thread1(String name) { this.name=name; } public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "运行 : " + i); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); } }
输出:
A运行 : 0
B运行 : 0
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
再运行一下:
A运行 : 0
B运行 : 0
B运行 : 1
B运行 : 2
B运行 : 3
B运行 : 4
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
但是start方法重复调用的话,会出现java.lang.IllegalThreadStateException异常。
Thread1 mTh1=new Thread1("A"); Thread1 mTh2=mTh1; mTh1.start(); mTh2.start();
输出:
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Unknown Source)
at com.multithread.learning.Main.main(Main.java:31)
A运行 : 0
A运行 : 1
A运行 : 2
A运行 : 3
A运行 : 4
二、实现java.lang.Runnable接口
/** *@functon 多线程学习 *@author 林炳文 *@time 2015.3.9 */ package com.multithread.runnable; class Thread2 implements Runnable{ private String name; public Thread2(String name) { this.name=name; } @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "运行 : " + i); try { Thread.sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { new Thread(new Thread2("C")).start(); new Thread(new Thread2("D")).start(); } }
输出:
C运行 : 0
D运行 : 0
D运行 : 1
C运行 : 1
D运行 : 2
C运行 : 2
D运行 : 3
C运行 : 3
D运行 : 4
C运行 : 4
三、Thread和Runnable的区别
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
package com.multithread.learning; /** *@functon 多线程学习,继承Thread,资源不能共享 *@author 林炳文 *@time 2015.3.9 */ class Thread1 extends Thread{ private int count=5; private String name; public Thread1(String name) { this.name=name; } public void run() { for (int i = 0; i < 5; i++) { System.out.println(name + "运行 count= " + count--); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); } }
输出:
B运行 count= 5
A运行 count= 5
B运行 count= 4
B运行 count= 3
B运行 count= 2
B运行 count= 1
A运行 count= 4
A运行 count= 3
A运行 count= 2
A运行 count= 1
从上面可以看出,不同的线程之间count是不同的,这对于卖票系统来说就会有很大的问题,当然,这里可以用同步来作。这里我们用Runnable来做下看看
/** *@functon 多线程学习 继承runnable,资源能共享 *@author 林炳文 *@time 2015.3.9 */ package com.multithread.runnable; class Thread2 implements Runnable{ private int count=15; @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "运行 count= " + count--); try { Thread.sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Main { public static void main(String[] args) { Thread2 my = new Thread2(); new Thread(my, "C").start();//同一个mt,但是在Thread中就不可以,如果用同一个实例化对象mt,就会出现异常 new Thread(my, "D").start(); new Thread(my, "E").start(); } }
输出:
C运行 count= 15
D运行 count= 14
E运行 count= 13
D运行 count= 12
D运行 count= 10
D运行 count= 9
D运行 count= 8
C运行 count= 11
E运行 count= 12
C运行 count= 7
E运行 count= 6
C运行 count= 5
E运行 count= 4
C运行 count= 3
E运行 count= 2
这里要注意每个线程都是用同一个实例化对象,如果不是同一个,效果就和上面的一样了!
总结:
实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
提醒一下大家:main方法其实也是一个线程。在java中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到CPU的资源。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用java命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。
四、线程状态转换
五、线程调度
线程的调度
六、常用函数说明
①sleep(long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)②join():指等待t线程终止。
使用方式。
join是Thread类的一个方法,启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。
Thread t = new AThread(); t.start(); t.join();
为什么要用join()方法
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
不加join。/** *@functon 多线程学习,join *@author 林炳文 *@time 2015.3.9 */ package com.multithread.join; class Thread1 extends Thread{ private String name; public Thread1(String name) { super(name); this.name=name; } public void run() { System.out.println(Thread.currentThread().getName() + " 线程运行开始!"); for (int i = 0; i < 5; i++) { System.out.println("子线程"+name + "运行 : " + i); try { sleep((int) Math.random() * 10); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " 线程运行结束!"); } } public class Main { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); } }
输出结果:
main主线程运行开始!
main主线程运行结束!
B 线程运行开始!
子线程B运行 : 0
A 线程运行开始!
子线程A运行 : 0
子线程B运行 : 1
子线程A运行 : 1
子线程A运行 : 2
子线程A运行 : 3
子线程A运行 : 4
A 线程运行结束!
子线程B运行 : 2
子线程B运行 : 3
子线程B运行 : 4
B 线程运行结束!
发现主线程比子线程早结束
加join
public class Main { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()+"主线程运行开始!"); Thread1 mTh1=new Thread1("A"); Thread1 mTh2=new Thread1("B"); mTh1.start(); mTh2.start(); try { mTh1.join(); } catch (InterruptedException e) { e.printStackTrace(); } try { mTh2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "主线程运行结束!"); } }
运行结果:
main主线程运行开始!
A 线程运行开始!
子线程A运行 : 0
B 线程运行开始!
子线程B运行 : 0
子线程A运行 : 1
子线程B运行 : 1
子线程A运行 : 2
子线程B运行 : 2
子线程A运行 : 3
子线程B运行 : 3
子线程A运行 : 4
子线程B运行 : 4
A 线程运行结束!
主线程一定会等子线程都结束了才结束
③yield():暂停当前正在执行的线程对象,并执行其他线程。
/** *@functon 多线程学习 yield *@author 林炳文 *@time 2015.3.9 */ package com.multithread.yield; class ThreadYield extends Thread{ public ThreadYield(String name) { super(name); } @Override public void run() { for (int i = 1; i <= 50; i++) { System.out.println("" + this.getName() + "-----" + i); // 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行) if (i ==30) { this.yield(); } } } } public class Main { public static void main(String[] args) { ThreadYield yt1 = new ThreadYield("张三"); ThreadYield yt2 = new ThreadYield("李四"); yt1.start(); yt2.start(); } }
运行结果: 第一种情况:李四(线程)当执行到30时会CPU时间让掉,这时张三(线程)抢到CPU时间并执行。 第二种情况:李四(线程)当执行到30时会CPU时间让掉,这时李四(线程)抢到CPU时间并执行。 sleep()和yield()的区别 sleep()和yield()的区别):sleep()使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。 sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield()方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程 另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield() 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。 ④setPriority(): 更改线程的优先级。 MIN_PRIORITY = 1 NORM_PRIORITY = 5 MAX_PRIORITY = 10 用法:
Thread4 t1 = new Thread4("t1"); Thread4 t2 = new Thread4("t2"); t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY);
⑤interrupt():中断某个线程,这种结束方式比较粗暴,如果t线程打开了某个资源还没来得及关闭也就是run方法还没有执行完就强制结束线程,会导致资源无法关闭
要想结束进程最好的办法就是用sleep()函数的例子程序里那样,在线程类里面用以个boolean型变量来控制run()方法什么时候结束,run()方法一结束,该线程也就结束了。
⑥wait()
Obj.wait(), 与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从 语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说 线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的 notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的 synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执 行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要 的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。
单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印ABC的问题了吧,这是一道比较经典的面试题,题目要求如下:
建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:
/** * wait用法 * @author DreamSea * @time 2015.3.9 */ package com.multithread.wait; public class MyThreadPrinter2 implements Runnable { private String name; private Object prev; private Object self; private MyThreadPrinter2(String name, Object prev, Object self) { this.name = name; this.prev = prev; this.self = self; } @Override public void run() { int count = 10; while (count > 0) { synchronized (prev) { synchronized (self) { System.out.print(name); count--; self.notify(); } try { prev.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws Exception { Object a = new Object(); Object b = new Object(); Object c = new Object(); MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a); MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b); MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c); new Thread(pa).start(); Thread.sleep(100); //确保按顺序A、B、C执行 new Thread(pb).start(); Thread.sleep(100); new Thread(pc).start(); Thread.sleep(100); } }
ABCABCABCABCABCABCABCABCABCABC
先 来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是 ThreadA->ThreadB->ThreadC->ThreadA循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定 唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是prev,就是前一个线程所持有的对象锁。还有一个就是自身对象 锁。主要的思想就是,为了控制执行的顺序,必须要先持有prev锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用 self.notify()释放自身对象锁,唤醒下一个等待线程,再调用prev.wait()释放prev对象锁,终止当前线程,等待循环结束后再次被 唤醒。运行上述代码,可以发现三个线程循环打印ABC,共10次。程序运行的主要过程就是A线程最先运行,持有C,A对象锁,后释放A,C锁,唤醒B。线 程B等待A锁,再申请B锁,后打印B,再释放B,A锁,唤醒C,线程C等待B锁,再申请C锁,后打印C,再释放C,B锁,唤醒A。看起来似乎没什么问题, 但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照A,B,C的顺序来启动,按照前面的思考,A唤醒B,B唤醒C,C再唤醒A。但是这种假 设依赖于JVM中线程调度、执行的顺序。wait和sleep区别
共同点:
1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
2. wait()和sleep()都可以通过interrupt()方法 打断线程的暂停状态 ,从而使线程立刻抛出InterruptedException。
如果线程A希望立即结束线程B,则可以对线程B对应的Thread实例调用interrupt方法。如果此刻线程B正在wait/sleep /join,则线程B会立刻抛出InterruptedException,在catch() {} 中直接return即可安全地结束线程。
需要注意的是,InterruptedException是线程自己从内部抛出的,并不是interrupt()方法抛出的。对某一线程调用 interrupt()时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出InterruptedException。但是,一旦该线程进入到 wait()/sleep()/join()后,就会立刻抛出InterruptedException 。
不同点:
1. Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等
2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
4. sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
所以sleep()和wait()方法的最大区别是:
sleep()睡眠时,保持对象锁,仍然占有该锁;
而wait()睡眠时,释放对象锁。
但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait()方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
七、常见线程名词解释
线程类的一些常用方法:
sleep(): 强迫一个线程睡眠N毫秒。
isAlive(): 判断一个线程是否存活。
join(): 等待线程终止。
activeCount(): 程序中活跃的线程数。
enumerate(): 枚举程序中的线程。
currentThread(): 得到当前线程。
isDaemon(): 一个线程是否为守护线程。
setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
setName(): 为线程设置一个名称。
wait(): 强迫一个线程等待。
notify(): 通知一个线程继续运行。
setPriority(): 设置一个线程的优先级。
八、线程同步
1、synchronized关键字的作用域有二种:
1)
是某个对象实例内,synchronized
aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线
程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的
synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法。它可以对类的所有对象实例起作用。
2、除了方法前用synchronized关键字,synchronized关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized(this){/*区块*/},它的作用域是当前对象;
3、synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法;
Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对synchronized关键字的作用进行深入了解才可定论。
总 的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分 类,synchronized可作用于instance变量、object reference(对象引用)、static函数和class literals(类名称字面常量)身上。
在进一步阐述之前,我们需要明确几点:
A.无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。
B.每个对象只有一个锁(lock)与之相关联。
C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
接着来讨论synchronized用到不同地方对代码产生的影响:
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都可以调用它们。
1. 把synchronized当作函数修饰符时,示例代码如下:
Public synchronized void methodAAA()
{
//….
}
这 也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个 同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized 关键字的方法。
上边的示例代码等同于如下代码:
public void methodAAA()
{
synchronized (this) // (1)
{
//…..
}
}
(1) 处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同 步机制的控制,造成数据混乱:(
2.同步块,示例代码如下:
public void method3(SomeObject so)
{
synchronized(so)
{
//…..
}
}
这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁:
class Foo implements Runnable
{
private byte[] lock = new byte[0]; // 特殊的instance变量
Public void methodA()
{
synchronized(lock) { //… }
}
//…..
}
注:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronized static void methodAAA() // 同步的static 函数
{
//….
}
public void methodBBB()
{
synchronized(Foo.class) // class literal(类名称字面常量)
}
}
代码中的methodBBB()方法是把class literal作为锁的情况,它和同步的static函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个Class产生的某个具体对象了)。
记得在《Effective Java》一书中看到过将 Foo.class和 P1.getClass()用于作同步锁还不一样,不能用P1.getClass()来达到锁这个Class的目的。P1指的是由Foo类产生的对象。
可 以推断:如果一个类中定义了一个synchronized的static函数A,也定义了一个synchronized 的instance函数B,那么这个类的同一对象Obj在多线程中分别访问A和B两个方法时,不会构成同步,因为它们的锁都不一样。A方法的锁是Obj这 个对象,而B的锁是Obj所属的那个Class。
九、线程数据传递
在 传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式 下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和 return语句来返回数据。
9.1、通过构造方法传递数据
在创建线程时,必须要建立一个Thread类的或其子类的实
例。因此,我们不难想到在调用start方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用(其实就是在
run方法中使用)。下面的代码演示了如何通过构造方法来传递数据:
package mythread; public class MyThread1 extends Thread { private String name; public MyThread1(String name) { this.name = name; } public void run() { System.out.println("hello " + name); } public static void main(String[] args) { Thread thread = new MyThread1("world"); thread.start(); } }
由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据 在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就 会造成很多不便。由于Java没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。 因此,要想避免这种情况,就得通过类方法或类变量来传递数据。
9.2、通过变量和方法传递数据
向对象中传入数据一般有两
次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的public的方法或变量(也可称之为字段)。然后在建立
完对象后,通过对象实例逐个赋值。下面的代码是对MyThread1类的改版,使用了一个setName方法来设置 name变量:
package mythread; public class MyThread2 implements Runnable { private String name; public void setName(String name) { this.name = name; } public void run() { System.out.println("hello " + name); } public static void main(String[] args) { MyThread2 myThread = new MyThread2(); myThread.setName("world"); Thread thread = new Thread(myThread); thread.start(); } }
9.3、通过回调函数传递数据
上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方 法都是main方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下 面代码的run方法中产生了3个随机数,然后通过Work类的process方法求这三个随机数的和,并通过Data类的value将结果返回。从这个例 子可以看出,在返回value之前,必须要得到三个随机数。也就是说,这个 value是无法事先就传入线程类的。
package mythread; class Data { public int value = 0; } class Work { public void process(Data data, Integer numbers) { for (int n : numbers) { data.value += n; } } } public class MyThread3 extends Thread { private Work work; public MyThread3(Work work) { this.work = work; } public void run() { java.util.Random random = new java.util.Random(); Data data = new Data(); int n1 = random.nextInt(1000); int n2 = random.nextInt(2000); int n3 = random.nextInt(3000); work.process(data, n1, n2, n3); // 使用回调函数 System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+" + String.valueOf(n3) + "=" + data.value); } public static void main(String[] args) { Thread thread = new MyThread3(new Work()); thread.start(); } }
林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka
以上是关于java多线程(超详细)的主要内容,如果未能解决你的问题,请参考以下文章