1,关于线程的概述
-
什么是进程?什么是线程?
- 进程是一个应用程序。
- 线程是一个进程中的执行场景/执行单元;一个进程可以启动多个线程。
-
在java语言中:
- 多个线程,堆内存和方法区内存共享。
- 栈内存独立,一个线程一个栈。
- 假设启动10个线程,会有十个栈内存空间,每个栈之间互不干扰;各自执行各自的,这就是多线程并发。
-
思考一个问题:
- 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束;main方法结束
只是主线程结束了,主栈空了;其他的栈(线程)可能还在压栈弹栈。
- 使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束;main方法结束
-
分析一个问题:对于单核CPU来说,真的可以做到真正的多线程并发吗?
- 对于多核的CPU电脑来说,真正的多线程并发是没问题的。
- 什么是真正的多线程并发?
- t1线程执行t1的, t2线程执行t2的,
t1、t2互不影响;这就是真正的多线程并发。
- t1线程执行t1的, t2线程执行t2的,
- 单核的CPU表示只有一个大脑:
- 不能做到真正的多线程并发,但可以做到给人一种"多线程并发"的感觉。
对于单核CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于
CPU的处理速度极快,多个线程之间频繁切换执行,给人的感觉是:多个事情
同时在做。
- 不能做到真正的多线程并发,但可以做到给人一种"多线程并发"的感觉。
-
java语言中实现线程的两种方式:
- 1,编写一个类,直接继承java.lang.Thread,重写run方法。
- 2,编写一个类,实现java.lang.Runnable接口,实现run方法。
- 注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承其他的类,更加灵活。
Java支持多线程机制,并且已经将多线程实现了,我们只需要继承就可以了。
- 线程的生命周期
- 新建状态:刚new出来的的线程对象;
- 就绪状态:调用完start()方法;
- 就绪状态线程又叫可运行状态,表示当前线程具有抢夺CPU时间片的权利(CPU时间片就是执行权);当一个线程抢夺到CPU时间片之后,就开始执行run方法,run方法的开始执行标志着线程进入运行状态。
- 运行状态:run方法开始执行;
- run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态
继续抢夺CPU时间片,当再次抢到CPU时间片之后,会重新进入run方法接着上一次的代码继续往下执行。
- run方法的开始执行标志着这个线程进入运行状态,当之前占有的CPU时间片用完之后,会重新回到就绪状态
- 阻塞状态:遇到阻塞事件;
- 在一个线程运行时,遇到阻塞事件,例如:接收用户键盘输入,或者sleep()方法等,此时线程会进入阻塞状态,
阻塞状态的线程会放弃之前占有的CPU时间片;当阻塞事件结束,需要再次回到就绪状态重新抢夺CPU时间片。
- 在一个线程运行时,遇到阻塞事件,例如:接收用户键盘输入,或者sleep()方法等,此时线程会进入阻塞状态,
- 死亡状态:run方法执行完成(结束)。
1.1,实现线程的第一种方式:
编写一个类,继承java.lang.Thread,重写run方法。
-
start()方法的作用:
- start()方法的作用:启动一个分支线程,在JVA中开辟一个新的栈空间。
- 只要新的栈空间开出来,start()方法就结束了;分支线程启动成功了。
- 启动成功的的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
- run方法在分支栈的底部,main方法在主栈的栈底部。run和main是平级的。
直接调用run方法,不会启动线程,也不会分配新的分支栈。
代码示例
public class ThreadTest02 {
public static void main(String[] args) {
// 这里是main方法,这里的代码属于主线程,在主栈中运行。
// 新建一个分支线程对象
MyThread myThread = new MyThread();
// 启动线程
myThread.start();// 这个代码执行极快,瞬间就结束了
// 直接调用run方法,不会启动线程,也不会分配新的分支栈。
// myThread.run();
// 这里的代码还是运行在主栈线程中
for (int i = 0;i < 1000; i++){
System.out.println("主线程--->" + i);
}
}
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0;i < 1000; i++){
System.out.println("分支线程--->" + i);
}
}
}
1.2实现线程的第二种方式:
- 编写一个类,实现java.lang.Runnable接口,实现run方法。
代码示例
public class ThreadTest03 {
public static void main(String[] args) {
// 创建一个可运行的对象
MyRunnable r = new MyRunnable();
// 将可运行的对象封装成一个线程对象
Thread t = new Thread(r);
// 合并代码
// Thread t = new Thread(new MyRunnable());
//启动线程
t.start();
for (int i = 0;i < 100; i++){
System.out.println("主线程--->" + i);
}
// --------使用匿名内部类------------------------------------------------------
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0;i < 100; i++){
System.out.println("分支线程(匿名内部类)--->" + i);
}
}
});
// 启动线程
t2.start();
for (int i = 0;i < 100; i++){
System.out.println("main主线程--->" + i);
}
}
}
// 这并不是一个线程类,是一个可运行的类;它还不是一个线程
class MyRunnable implements Runnable{
@Override
public void run() {
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0;i < 100; i++){
System.out.println("分支线程--->" + i);
}
}
}
1.3 线程中常用的三个方法
- 获取当前线程对象:
static native Thread currentThread()
- 获取线程对象的名字:
final String getName()
- 修改线程对象的名字
setName(String name)
- 当线程线程没有设置名字的时候,默认名字是:
Thread-0 Thread-1 Thread-2 Thread-3 ...
代码示例
public class ThreadTest04 {
public static void main(String[] args) {
// 获取当前线程
// 这个代码出现在main方法中,当前线程就是主线程
Thread mt = Thread.currentThread();
// 获取线程名字
System.out.println(mt.getName()); // main
//=================================================================
// 创建线程对象
MyThread2 t = new MyThread2();
// 获取线程名字
System.out.println(t.getName());// Thread-0
// 设置线程的名字
t.setName("t1");
// 获取线程名字
System.out.println(t.getName());// t1
// 启动线程
t.start();
// 创建线程对象
MyThread2 t2 = new MyThread2();
t2.setName("t2");
// 启动线程
t2.start();
}
}
class MyThread2 extends Thread{
@Override
public void run() {
// 获取当前线程
Thread currentThread = Thread.currentThread();
// 编写程序,这段程序运行在分支线程中(分支栈)
for (int i = 0;i < 100; i++){
System.out.println(currentThread.getName() + "-->" + i);
}
}
}
2 线程线程的休眠和唤起(终止)
2.1 关于线程的sleep方法:
-
Static void sleep (long millis)
- 1,静态方法:Thread.sleep(1000);
- 2,参数是毫秒。
- 3,作用是让"当前线程"进入休眠,进入"阻塞状态";放弃占有的CPU时间片,让其他线程使用。
-
强行终止一个线程的执行:
- void interrupt();这种终止睡眠的方式依靠java异常处理机制。
代码示例
public class ThreadTest05 {
public static void main(String[] args) {
System.out.println("hello");
Thread t = new Thread(new MyRunnable2());
t.setName("tt");
t.start();
// 希望五秒之后,t线程醒来。
try {
Thread.sleep(1000 * 5);// 休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 五秒钟之后执行下面代码
System.out.println("word");
// 终止t线程的睡眠
// 这种终止睡眠的方式依靠java异常处理机制
t.interrupt();
}
}
class MyRunnable2 implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "-->" + "begin");
try {
// 睡眠一年
Thread.sleep(1000 * 60 * 60 * 24 *365);
} catch (InterruptedException e) {
// 打印异常信息
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->" + "end");
}
}
2.2 合理的终止一个线程的执行:(很常用)
public class ThreadTest06 {
public static void main(String[] args) {
MyRunnable3 runnable3 = new MyRunnable3();
Thread t = new Thread(runnable3);
t.setName("t3");
t.start();
// 希望五秒之后,t线程醒来。
try {
Thread.sleep(1000 * 5);// 休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止线程 把runBool设为:false
runnable3.runBool = false;
}
}
class MyRunnable3 implements Runnable{
// 打一个布尔标记
boolean runBool = true;
@Override
public void run() {
for (int i = 0;i < 100;i++){
if (runBool){
System.out.println(Thread.currentThread().getName() + "-->" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 终止当前线程
return;
}
}
}
}
3,关于线程的调度
- 1,常见的线程调度模型:(属于了解内容)
- 抢占式调度模型:
- 哪个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
- java采用的就是抢占式调度模型。
- 均分式调度模型:
- 平均分配CPU时间片,每个线程占有的CPU时间片时间长度一样;平均分配,一切平等。
- 抢占式调度模型:
- java中和线程调度有关系的方法:
-
实例方法:
void setPriority(int newPriority) 设置线程的优先级 int getPriority() 获取线程优先级 最低优先级是1 默认优先级是5 最高优先级是10 void join() 合并线程; 在当前t1线程中new一个t2线程,t2.join(); 然后当前t1线程进入阻塞;t2线程执行, 直到t2线程结束;当前t1线程才可以继续执行。
-
静态方法:
static void yield() 让位方法;暂停当前正在执行的线程对象,并执行其他线程; yield()方法不是阻塞方法,让当前线程让位,让给其他线程使用; yield()方法的执行会让当前线程从"运行状态"回到"就绪状态"。
-
代码示例
以下代码是包含了:线程优先级、线程让位、合并线程;大家复制代码运行时,最好根据"=====..."分割线,注释一部分执行看效果。
public class ThreadTest08 {
public static void main(String[] args) {
System.out.println("最高优先级:" + Thread.MAX_PRIORITY);
System.out.println("最低优先级:" + Thread.MIN_PRIORITY);
System.out.println("默认优先级:" + Thread.NORM_PRIORITY);
// 获取当前线程对象
Thread tc = Thread.currentThread();
// 获取当前线程的优先级
System.out.println(tc.getName() +"线程的优先级:" + tc.getPriority());//5
// 更改当前线程优先级
tc.setPriority(6);
System.out.println(tc.getName() +"线程的优先级:" + tc.getPriority());//6
// ===================线程让位================================================================
System.out.println("===================线程让位==========================");
Thread tc1 = new Thread(new MyRunnable4());
tc1.setName("tc1");
tc1.start();
for (int i = 0;i < 1000; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
// ===================合并线程================================================================
System.out.println("===================合并线程=============================");
System.out.println("main begin");
Thread tc2 = new Thread(new MyRunnable5());
tc2.setName("tc2");
tc2.start();
// 合并线程
try {
tc2.join();// 合并到当前线程中,当前线程阻塞,tc2线程执行直到结束。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
class MyRunnable4 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 1000; i++){
// 每50个让位一次
if ( i % 50 == 0){
Thread.yield();// 当前线程暂停一下,让给主线程
}
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
class MyRunnable5 implements Runnable{
@Override
public void run() {
for (int i = 0;i < 100; i++){
System.out.println(Thread.currentThread().getName() + "-->" + i);
}
}
}
4,关于线程安全
-
什么时候数据在多线程并发的环境下会存在安全问题呢
- 1,多线程并发
- 2,有共享数据。
- 3,共享数据有修改的行为。
- 满足以上3个条件之后,就会存在线程安全问题。
-
怎么解决线程安全问题
- 当多线程并发的环境下,有共享数,并且这个数据还会被修改,就会存在线程
- 安全问题,如何解决:
- 线程排队执行。(不能并发)
- 用排队执行解决线程安全问题。
- 这种机制称为:线程同步机制。
-
线程同步机制涉及两个专业术语:
- 同步编程模型:(同步就是排队)
- 线程t1和线程t2:t1线程必须等待t2线程执行结束再执行,或者t2线程必须等待t1线程执行结束再执行,
两个线程之间发生了等待关系。这就是同步编程模型,效率较低,线程排队执行
- 线程t1和线程t2:t1线程必须等待t2线程执行结束再执行,或者t2线程必须等待t1线程执行结束再执行,
- 异步编程模型:(异步就是并发)
- 线程t1和线程t2:各自执行各自的互不影响,谁也不等谁,这种编程模型叫做:异步编程模型。
- 同步编程模型:(同步就是排队)
-
java语言中的三大变量谁可能存在线程安全问题:
- 实例变量:在堆中;堆只有一个,是多线程共享的,所以可能存在线程安全问题。
- 静态变量:在方法区中;方法区只有一个,是多线程共享的,所以可能存在线程安全问题。
- 局部变量:在栈中;一个线程一个栈,所以局部变量永远不会有线程安全问题。(局部变量不共享)
-
开发中如何解决线程安全问题?
- 第一种方案:尽量使用局部变量代替"实例变量"和"静态变量"。
- 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内容就不共享了。
(一个线程对应一个对象) - 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了,
线程同步机制。
4.1,模拟多线程高并发Demo
- Account类的withDraw方法的两种写法:
- 1,不使用线程同步机制,多线程对同一银行账户进行取款,出现线程安全问题。
- 2,使用线程同步机制,多线程对同一银行账户进行取款,解决线程安全问题。(synchronized三种写法如下:)
-
2.1,synchronized同步代码块:
synchronized (){ // 线程同步代码块 } synchronized后面小括号中传的"数据"是十分关键的,这个数据必须是多线程贡献的数据, 才能达到多线程排队。 synchronized()小括号中写什么? 要看你想让哪些线程同步。 假设t1,t2,t3,t4,t5,有五个线程;你希望t1,t2,t3排队,t4,t5不需要排队; 你一定要在()中写一个t1,t2,t3共享的对象;这个对象对于t4,t5不共享。
-
2.2,在实例方法上使用synchronized
- 表示共享对象一定是this,并且同步代码块是整个方法体。
-
2.3,在静态方法上使用synchronized
- 表示找类锁,类锁永远只有一把,就算创建了100个对象,类锁也只有一把。
-
代码示例
public class ThreadSafeTest01 {
public static void main(String[] args) {
// 创建账户对象(只创建一个)
Account act = new Account("act-001",10000);
// 创建两个线程
Thread t1 = new AccountThread(act);
Thread t2 = new AccountThread(act);
// 设置线程名字
t1.setName("t1");
t2.setName("t2");
// 启动线程取款
t1.start();
t2.start();
}
}
class AccountThread extends Thread{
// 两个线程必须共享同一个账户对象
private Account act;
// 通过构造方法传递过来账号对象
public AccountThread(Account act){
this.act = act;
}
@Override
public void run() {
super.run();
// 这里执行取款操作
double money = 5000;
// 也可以采用这种写法;去掉方法里面写的synchronized。
// 多线程并发执行withDraw整个方法;
// 这种写法会扩大同步范围,效率变得更低了。
// synchronized (act){
// act.withDraw(money);
// }
act.withDraw(money);
System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款成功:" + money + ";剩余余额:" + act.getBalance());
/*
采用withDraw方法的第一中方式:
出现了高并发(取款了2次5000,余额应为0)
打印结果余额缺为:5000:
t1对act-001取款成功:5000.0;剩余余额:5000.0
t2对act-001取款成功:5000.0;剩余余额:5000.0
*/
}
}
class Account{
// 账号
private String actno;
// 余额
private double balance;
// 创建一个实例对象
Object obj = new Object();
public Account() {
}
public Account(String actno, double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String acton) {
this.actno = acton;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public Account(double balance) {
this.balance = balance;
}
// 取款方法
// 1,不使用线程同步机制,多线程对同一银行账户进行取款,出现线程安全问题。
// public void withDraw(double money){
// // 取款之前的余额
// double before = this.getBalance();
// // 取款之后的余额
// double after = before - money;
//
// // 模拟一下网络延迟
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// // 更新余额
// this.setBalance(after);
// }
// 2,使用线程同步机制,多线程对同一银行账户进行取款,解决线程安全问题。
public void withDraw(double money){
// 这里的共享对象是:账户对象,那么this就是账户对象。
// synchronized (this){
synchronized (obj){ // 实例对象只有一个 也属于共享对象
// 取款之前的余额
double before = this.getBalance();
// 取款之后的余额
double after = before - money;
// 模拟一下网络延迟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 更新余额
this.setBalance(after);
}
}
/*
2.1,在实例方法上使用synchronized,表示整个方法体都需要同步,
可能会无故扩大同步的范围,会导致程序的执行效率降低。这种方式不常用。
在实例方法上使用synchronized使用在实例方法上的优点:
代码写的少了,节俭了。
如果共享的对象就是this,并且需要同步的代码块是整个方法体,建议使用这种方式。
*/
// public synchronized void withDraw(double money){
// // 取款之前的余额
// double before = this.getBalance();
// // 取款之后的余额
// double after = before - money;
//
// // 模拟一下网络延迟
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// // 更新余额
// this.setBalance(after);
// }
}
上篇:JavaSE进阶九 IO流二
下篇:JavaSE进阶十 线程二