目标
- 了解线程和进程的关系
- 了解线程并发执行的原理
- 熟练使用两种方式创建线程
- 理解同步机制(锁)。
多线程学习的路径
理解什么是线程 -> 线程的基本使用 -> 理解并使用同步 -> 学习并掌握JUC -> 理解线程执行的底层逻辑
一:线程的概念
进程与线程的关系
进程:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程 执行原理 ( 并发执行原理 )
CPU核心: 每一个计算机中CPU核心, 就相当于人的一个大脑, 可以执行一件事情。
时间片: 将CPU未来可以用于执行的时间 ,碎片化, 拆分成一个个的小的时间片段. 每一个时间片段的大小 可以都不足一毫秒,程序在争抢到时间片段后, 会进行执行 。
线程创建的三种方式
Demo01.线程创建的方式一 继承Thread
Demo02.线程创建的方式二 实现Runnable
Demo03.线程创建的方式三 方式一的简写格式,通过匿名内部类的方式继承Thread
package com.kkb;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i =0;i<1000;i++){
System.out.println("runnable:"+i);
}
}
}
Demo01.线程创建的方式一 继承Thread
package com.kkb;
public class Demo01 {
static class MyThread1 extends Thread{
@Override
public void run() {
heihei("小红");
}
}
public static void main(String[] args) {
Thread t = new MyThread1();
t.start();
heihei("小明");
}
public static void heihei(String text){
for (int i=0;i<1000;i++){
System.out.println(text+": i="+i);
}
}
}
Demo02.线程创建的方式二 实现Runnable
package com.kkb;
public class Demo02 {
public static void main(String[] args) {
MyRunnable run =new MyRunnable();
Thread t = new Thread(run);
t.start();
for (int i =0;i<1000;i++){
System.out.println("main:"+i);
}
}
}
Demo03.线程创建的方式三 方式一的简写格式,通过匿名内部类的方式继承Thread
package com.kkb;
public class Demo03 {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println("t: "+i);
}
}
}.start();
for (int i=0;i<1000;i++){
System.out.println("m: "+i);
}
}
}
Thread 类 常用方法
常用的静态方法
静态方法作用于,调用的线程。
- static void sleep(int 毫秒数) ***
此方法执行时 所在的线程睡眠 , 用于降低线程的活跃度 .
- static Thread currentThread();
获取此方法执行时 所在线程的 线程对象.
- static void yield();
此方法执行时所在的线程, 让出当前时间片. 用于降低线程的活跃度
package com.kkb;
public class Demo04 {
public static void main(String[] args) throws Exception {
Thread t =new Thread(){
@Override
public void run() {
for (int i=0;i<1000;i++){
System.out.println(Thread.currentThread().getName()+":t="+i);
};
}
};
t.setName("新线程");
t.start();
for (int i=0;i<1000;i++){
//降低线程活跃度,让它一秒打印一次
//Thread.sleep(1000);
//让出时间片
Thread.yield();
System.out.println(Thread.currentThread().getName()+":m="+i);
Thread.yield();
}
}
}
- 常用的非静态方法
- void start() ***
启动此线程所表示的线程对象, 并在新的线程中执行run方法
- void setName(String name) | String getName(); 了解
设置/获取 线程名称; 线程拥有默认的名称
- stop() 了解
停止线程(已过时 , 不建议使用, 因为会造成固有的不安全性);
- interrupt() 了解
中断线程!
此方法用于在某一个线程上 加入 中断标记.
这个线程被加入标记后, 它正在执行的逻辑,如果触发一些特殊的方法, 会出现异常 !
通常此操作用于通知线程停止 . 具体是否停止, 由线程自身决定.
package com.kkb;
public class Demo05 {
public static void main(String[] args) {
Thread t1 =new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//中断异常
//e.printStackTrace();会报错,但程序不会仍旧继续
//中断线程,杀死线程最合法的方式,stop方法不安全
break;
}
System.out.println("i="+i);
}
}
};
t1.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();
}
}
守护线程
- setDaemon(boolean flag) 了解
传入true设置线程为守护线程 ! 默认一个线程为用户线程 .
线程共分为两大类
1. 用户线程
通常情况下, 我们自己开启的线程都是用户线程, 所有的用户线程都是一条单独的执行路径, 一个软件的所有用户线程执行完毕, 程序会死亡 .
2. 守护线程
程序中存在一种特殊的线程, 叫做守护线程, 可以理解为 : 守护用户线程 .
当程序中所有的用户线程都死亡了. 守护线程无论是否执行完毕, 都会自动死亡.
Java中的垃圾回收器 GC 其实就是守护线程.
package com.kkb;
public class Demo06 {
public static void main(String[] args) {
Thread Liming = new Thread(){
@Override
public void run() {
for (int i=0;i<60;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("黎明:嗯哼哈嘿。。。");
}
System.out.println("黎明完事走了。");
}
};
Thread Dage = new Thread(){
@Override
public void run() {
while (true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("大哥:望风中。。。");
}
}
};
Dage.setDaemon(true);
Liming.start();
Dage.start();
}
}
线程的优先级
- setPriority(int 优先级) 了解
线程优先级: 取值1-10 , 表示线程的10个优先等级. 10最高 1最低
优先级的作用是: 让线程更有可能争抢到时间片 .
系统提供了建议使用的三种值:
1: 较低的优先级
5: 中等的优先级
10: 较高的优先级
package com.kkb;
public class Demo07 {
public static void main(String[] args) {
MyThread m1=new MyThread();
MyThread m2=new MyThread();
MyThread m3=new MyThread();
m1.setPriority(1);
m1.setName("低优先级");
m2.setPriority(5);
m2.setName("中优先级");
m3.setPriority(10);
m3.setName("高优先级");
m1.start();
m2.start();
m3.start();
}
static class MyThread extends Thread{
@Override
public void run() {
for (int i=0;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
}
注意: 调整一个线程的优先级, 并不能保证一个线程绝对能争抢到时间片, 只是几率大了一丢丢 !
1和10的区别大概可以理解为: 50% 和51%
线程默认优先级
一个线程的默认优先级 与 启动它的线程完全一致 ;
main线程的优先级为5
线程的生命周期
1.创建
Plain Text
public class MyThread extends Thread{
@Override
public void run() {
//...
}
}
//新建就是new出对象
MyThread thread = new MyThread();
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
2.就绪
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。
Java虚拟机 会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
3.运行
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。
添加到了线程列表中,如果OS调度选中了,就会进入到运行状态
4.阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
- 1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 2、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 3、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)。
- 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
- 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
- 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
- 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
- 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
- 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。
5.死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:
- run()方法执行完成,线程正常结束。
- 线程抛出一个未捕获的Exception或Error。
- 直接调用该线程的stop()方法来结束该线程——该方法会产生固有的不安全性,不推荐.
线程安全问题
同步即排队 . 异步即并行.
当多个线程同时操作一段数据时,容易发生问题。
同步就是线程安全, 效率较低! 线程安全可以有效的结果 临界资源出错问题 !
异步表示线程非安全, 效率高
线程同步的两种实现方式
两种方式 都是通过 加锁 来完成同步操作的.
记住一点: 在同步代码块时 我们手动指定锁对象时 . 多个线程必须使用同一个锁 ,才可以完成同步操作 !
同步代码块
Plain Text
格式:
synchronized(Object 锁对象){
}
package com.kkb;
//同步:排队 多线程排队执行
//异步:并行 多线程同时执行
public class Demo08 {
public static int count =10;
//创建锁对象
static Object o =new Object();
public static void haha(){
synchronized (o){
if (1==1){}
}
}
public static void heihei(){
synchronized (o){//同步代码块
if (count > 0) {
System.out.println("余票充足,卖票中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票成功,余票:" + --count);
} else {
System.out.println("余票不足,很遗憾");
}
}
}
public static void main(String[] args) {
//线程一
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
//线程二
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
//线程三
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
}
}
同步方法
非静态的同步方法的锁对象为this
静态方法的锁对象 , 是我们的类信息对象 (类名.class).
格式
权限修饰符 synchronized 返回值声明 方法名(形式参数列表){
}
package com.kkb;
public class Demo09 {
public static int count =10;
//创建锁对象
static Object o =new Object();
public static synchronized void heihei(){
if (count > 0) {
System.out.println("余票充足,卖票中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("卖票成功,余票:" + --count);
} else {
System.out.println("余票不足,很遗憾");
}
}
public static void main(String[] args) {
//线程一
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
//线程二
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
//线程三
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
heihei();
}
}
}.start();
}
}
同步方法 与 同步代码块, 何时使用, 选择哪个?
同步代码块 粒度更小 . 更灵活 !
同步方法的操作 最小单位就是一个方法所有的 代码 .
同步方法因为默认使用了this作为锁对象. 所以它存在一个特殊的操作:
同一个对象中的 所有同步方法, 均使用this这一把锁, 所以可以达到 多方法同时上锁的需求 !
公平锁和不公平锁
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
线程同步问题
package com.kkb;
public class Demo10 {
public static void main(String[] args) {
heihei h = new heihei();
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
//线程排队,静态方法用的是同一把锁
//heihei.heihei1();
//非静态方法的锁是this,this是同一个对象吗?
//答案是的,还是会排队
//h.heihei2();
//但如果用的不是一个对象,就不会排队了
new heihei().heihei2();
}
}
}.start();
new Thread(){
@Override
public void run() {
for (int i=0;i<10;i++){
//heihei.heihei1();
//h.heihei2();
new heihei().heihei2();
}
}
}.start();
}
static int count = 0;
static class heihei{
// 静态的同步方法
public static synchronized void heihei1(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count++);
}
// 非静态的同步方法
public synchronized void heihei2(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count++);
}
}
}
线程死锁
package com.kkb;
public class Demo11 {
public static void main(String[] args) {
//线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}
static class MyThread extends Thread{
private Culprit c;
private Police p;
MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
static class Culprit{
//绑匪类
public void say(Police p){
System.out.println("绑匪:你放了我,我放人质");
p.fun();
}
public void fun(){
System.out.println("绑匪被放走了,也释放了人质");
}
}
static class Police{
//警察类
public void say(Culprit c){
System.out.println("警察:你放了人质,我放过你");
c.fun();
}
public void fun(){
System.out.println("警察救到了人质,也抓到了绑匪");
}
}
}
多线程通信问题, 生产者与消费者问题
package com.kkb;
public class Demo12 {
/**
* 多线程通信问题, 生产者与消费者问题
* @param args
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndSaste("老干妈小米粥","香辣味");
}else{
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
//true 表示可以生产
private boolean flag = true;
public synchronized void setNameAndSaste(String name,String taste){
if(flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag) {
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}