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 -总结
以上就是我对多线程初级的所有总结,希望对大家有所帮助。