目录
程序、进程、线程
CPU的单核、多核
并发、并行
多线程的创建、使用
Thread类
常用方法
多线程创建方式一:继承于Thread类
给线程命名:
线程优先级
多线程创建方式二:实现Runnable接口
两种方式的对比
多线程创建方式三:有返回值:Callable接口
Callable接口的使用
多线程创建方式四:线程池
线程的生命周期
线程安全问题
线程同步synchronized
synchronized同步代码块
因此,同步代码块既不能框的太少,也不能框的太多
synchronized同步方法
死锁
公平锁、非公平锁
jdk5之后提供了ReentrantLock类
wait和notify、notifyAll
wait和sleep的区别
为什么同步代码块不能框多也不能框少
线程通信:生产者消费者源码与解析
程序:静态的代码,没有运行起来的才叫程序
eg:文件夹中的所有文件
整个文件夹内包括的所有静态代码的集合就是程序
进程:是程序的一次运行or正在运行的一个程序,是一个动态的过程,并且有自身的产生、存在、消亡的过程——生命周期
eg:运行中的QQ、运行中的LOL、运行中的360安全卫士
线程:进程可以细分为线程,是程序内部的一条执行路径。如果一个进程可以同时并行执行多个线程,就是支持多线程的。
线程是调度和执行的单位,有独立的运行栈和程序计数器,线程切换的开销小
一个进程中多个线程共享相同的资源,因此可以进程间通信更高效,但也带来了安全隐患
eg:同时扫漏洞、扫毒、清理垃圾就是多线程,多线程增加了cpu利用率
eg:图形化界面的基本都是多线程
一个java应用程序java.exe至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程
单核CPU:实际上是一种假的多线程,表象的多线程实际上是快速在不同线程之间切换(时分复用),是一种并发
eg:同一个进程,使用单核cpu,单线程比多线程更快,因为没有线程切换时间
多核CPU:可以实现并行
eg:现在的很多手机都是8核心,但这8核并不是完全相同,当手机写备忘录时,调用功耗小功能更弱的核,玩游戏时调用更强但功耗大的核
并发:一个CPU同时执行多个线程
并行:多个CPU同时执行多个线程
java语言JVM允许程序运行多个线程,通过java.lang.Thread类来体现
查API文档
JDK定义了接口Runnable,内含run()方法,这个方法可以做任何事,需要重写
线程Thread类是Runnable的实现
Thread类包含了多个属性和方法
常用方法
- void start(): 启动线程,并执行对象的run()方法
- void run(): 线程在被调度时执行的操作
- String getName(): 返回线程的名称
- void setName(String name):设置该线程名称
- static Thread currentThread(): 返回当前线程。在Thread子类中就 是this,通常用于主线程和Runnable实现类
- static void yield():线程让步。暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法。
- join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止,低优先级的线程也可以获得执行。该方法需要try-catch
- static void sleep(long millis):(指定时间:毫秒) 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。 抛出InterruptedException异常。该方法需要try-catch
- stop(): 强制线程生命期结束,不推荐使用
- boolean isAlive():返回boolean,判断线程是否还活着
线程休眠:sleep
public static void sleep(long millis)throws InterruptedException
设置休眠的毫秒数,一到时间自动唤醒
public static void sleep(long millis,int nanos)throws InterruptedException
设置休眠的毫秒数和纳秒数,一到时间自动唤醒
线程中断:interrupt
public boolean isInterrupted()
//m1.isInterrupted()判断m1是否中断
public void interrupt()
//m1.isterrupt中断m1
线程强制执行:join
Thread类中的join()方法原理
- 一般是用在main方法中,用于占用主线程。
- 副线程必须先start再join才有效
- join方法的本质就是用调用方线程给主线程main上锁直到该线程结束才释放
- 遇到join时,主线程立即被占用,不会存在安全问题,因为本质就是同步机制
- 如果同时存在两个以上的副线程,并且同时对main用join上锁,那么main线程的恢复执行以最后一次锁结束为准,不同线程之间不受影响,因为他们占用的都是main线程而没有彼此占用
class J1Thread extends Thread{
@Override
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread().getName()+"\t11111111111");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class J2Thread extends Thread{
public void run() {
for(int i=1;i<100;i++){
System.out.println(Thread.currentThread().getName()+"\t22222222222");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
J1Thread j1 = new J1Thread();//创建线程对象
J2Thread j2 = new J2Thread();//创建线程对象
for(int i =1;i<100;i++) {//主线程main真正开始的地方
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程开始"+i);
}//执行完for循环体才会轮得到后面j1的开始
j1.start();//先创建j1的线程
j1.join();//用j1给主线程main上锁,知道j1结束才释放
for(int i =1;i<100;i++) {//主线程main继续
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程中途"+i);
}//执行完for循环体才会轮得到后面j2的开始
j2.start();
for(int i =1;i<100;i++) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"\t主线程结束"+i);
if(i==90)//for循环体途中用join上锁
j2.join();//然后j2占用main线程,把剩余部分执行完毕
}
}
}
线程让步:yield()
本质:多线程并发执行是多个线程交替执行,对于某一个线程并不会执行一下就马上释放,而是会持续一个随机的很短的时间。因此通过yield方法可以强制让正在占用资源的线程让出资源,让包括自己在内的所有线程去争夺资源。因此
- yield没有join那么强硬,join是直接上锁,yield只是放弃资源让大家重新争夺
- yield也没有sleep那么稳定,yield有极小概率没有任何让步效果,而sleep则稳定延迟
- 执行次数足够多时yield也能明显看到让步效果
- 同样的yield无论写多少个都一样,而同样的sleep写10个就有10倍的延迟效果
class MyThread1 implements Runnable{
public void run() {
for (int i = 100; i < 200; i++) {
System.out.println("11111111礼让");
yield();
}
}
}
class YouThread1 implements Runnable{
public void run() {
for (int i = 100; i < 200; i++) {
System.out.println("22222222");
}
}
}
public class YiledTest {
public static void main(String[] args) {
MyThread1 M1 = new MyThread1();
YouThread1 Y1 = new YouThread1();
Thread T1 = new Thread(M1);
Thread T2 = new Thread(Y1);
T1.start();
T2.start();
}
}
这一礼让基本把自己让没了
- 创建一个Thread的子类MyThread
- 重写run()方法
- new一个Mythread的对象
- 利用对象调用start()方法运行
如果最终是用对象调用run()方法,则没有创建多线程,执行的顺序还是单线程的顺序
例如:
public class ThreadTest {
public static void main(String[] args) {
new MyThread().start();//0号线程(空参构造)
new MyThread().start();//1号线程(空参构造)
new YouThread().start();//2号线程(空参构造)
//如果是 new MyThread().run();
// new YouThread().run();
//那就是先执行上面的全部,然后执行下面的全部
}
}
class MyThread extends Thread {
public void run() {
for (int i = 1; i < 1999; i++) {
if (i % 20 == 0) {
System.out.println(Thread.currentThread().getName()+":"+i);
}else{
try {//sleep方法必须要try-catch异常处理
sleep(1);//暂停线程1ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class YouThread extends Thread{
public void run() {
for (int j = 1; j < 100; j++) {
System.out.println(this.getName()+":匿名子类多线程测试");
try {
sleep(20);//暂停线程20ms
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果
同一个MyThread类的对象m1只能调用一次start,若想再次调用必须再new一个m2,或者直接匿名对象调用。
在线程类MyThread的定义中,this.相当于Thread.currentThread().
如果创建一个一次性线程,可以造一个匿名子类
new Thread(){ public void run(){ 方法体内容 } }.start();
setName()方法、构造器方法
注意:用构造器重命名需要在Thread子类MyThread中重写构造方法,因为构造方法不继承
class MyThread extends Thread {
public MyThread(String str) {
super(str);
}
public void run() {
MyThread m1 = new MyThread();
m1.setName("重命名xxx");
m1.start();
//先重命名后调用start
或者
MyThread m2 = new MyThread("重命名yyy");
即便是重命名了,每个创建的线程本身的编码private static int threadInitNumber;并不会改变,再次new一个线程,threadInitNumber仍然会在上一个基础上+1,并且与哪类Thread子类无关
例如:
public class ThreadTest {
public static void main(String[] args) {
new MyThread("线程一").start();//threadInitNumber=0
new MyThread("线程二").start();//threadInitNumber=1
new YouThread().start();//threadInitNumber=2
new Thread(){//new一个匿名子类的匿名对象 threadInitNumber=3
public void run(){
for (int i=1;i<2000;i++){
System.out.println(this.getName()+"匿名子类重写run方法");
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
对于主线程,原名main,更名方法如下
...main...{
MyThread m1 = new MyThread();
m1.setName("线程1");
Thread.currentThread().setName("主线程");
for(int i= 1;i<2000;i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
运行结果
线程优先级有1~10十种等级,其中定义了三个静态常量,最小值,默认值,最大值
优先级可以通过setPriority()方法设置,其中
- setPriority(10)相当于setPriority(MAX_PRIORITY)
- setPriority(1)相当于setPriority(MIN_PRIORITY)
- 不设置相当于setPriority(NORM_PRIORITY)
- 设置完优先级,线程的执行仍然是多线程同时进行,只不过从概率角度讲,同一时间PRIORITY高的线程更容易执行,因此优先级高的线程更容易早执行完。
- 优先级差别越大效果越明显,线程运行内容越多效果越明显(大数定律)
public class PriorityTest {
public static void main(String[] args) {
MyThread m1 = new MyThread("副线程1");
m1.setPriority(Thread.MAX_PRIORITY);
MyThread m2 = new MyThread("副线程2");
m2.setPriority(Thread.MAX_PRIORITY);
m1.setPriority(1);
m2.setPriority(NORM_PRIORITY);
//也可以m2.setPriority(5);
//不设置优先级就默认m2.setPriority(NORM_PRIORITY);
m2.start();
m1.start();
}
}
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
for(int i=1;i<100;i++){
System.out.println(getName()+":"+i);
}
}
}
运行结果:可以看到优先级高的线程很快就执行完了
- 创建一个实现了Runnable接口的类,这个类可以再派生子类
- 用这个实现类or其子类去实现Runnable中的抽象方法run()
- new这个实现类的对象
- 把这个对象作为参数传给Thread,创建Thread对象
- 经过两次new,用最终的Thread对象调用start()
class MyrunThread implements Runnable {
public void run() {
for (int i = 1; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class RunnableTest {
public static void main(String[] args) {
new Thread(new MyrunThread()).start();
//一步写法
MyrunThread m1 = new MyrunThread();
Thread M1 = new Thread(m1);
//两步写法
M1.start();
}
}
运行结果
1,开发中优先选择Runnable接口方式,原因如下
- 该方法没有单继承的局限性
- 适合处理多线程共享数据的情况
2,联系:public class Thread implements Runnable
3,相同点:
- 都需要重写run()
- 都要用最终对象调start()而不是调run()
4,区别:
- 继承方式new一次,接口方式new两次
使用Runnable接口可以解决多线程的单继承问题,但是Runnable接口以及其Thread实现中的run方法都没有返回值,Callable接口解决了返回值问题
与Runnable相比,Callable功能更强大
java.lang.Runnable是JDK1.0产物
java.util.concurrent.Callable是JDK5.0产物
- Runnable的run方法 相当于 Callablle的call方法(有返回值): 返回值由Future接口获取
- call()方法可以抛异常: run方法如果出现异常只能try-catch在内部处理,而call方法出现异常可以throws
- call()方法支持泛型的返回值 :可选设置项,避免向下转型带来的安全隐患
- 需要借助FutureTask类,比如获取返回结果
java.util.concurrent.Callable 下 Callable 接口的定义
@FunctionalInterface
public interface Callable{
public V call() throws Exception;
}
class Mythread implements Callable<泛型类X>{ public 返回值类型(类名X) call () throws 错误类型 { 方法体 return 能转成X的对象 } } public class Test{ public static void main(String[] args){ Mythread M1 = new Mythread(); FutureTask F1 = new FutureTask(M1);//和Runnable接口相比,多了这一步 Thread T1 = new Thread(F1,"线程名"); T1.start();//start运行call方法,但是不一定必须接收返回值 System.out.println(F1.get()); } }
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
class Mythread implements Callable{
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1 ; i<100 ; i++){
if(i%4==0){
System.out.println(i);
sum+=i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Mythread M1 = new Mythread();//实现Callable接口实例
FutureTask F1 = new FutureTask(M1);//传入FutureTask中
Thread T1 = new Thread(F1);//传入Thread中
T1.start();
System.out.println(F1.get());
//start运行call方法,但是不一定必须接收返回值
}
}
开发中一般都是用线程池
如果创建的多个线程将对同一个共享数据进行操作,就可能导致读入和改写数据的各种错误。为了避免冲突,不同线程在对共享数据进行操作的时候应该有一个同步机制,甲访问该共享数据的时候应该对其上锁,乙丙丁等线程在上锁处停滞,等待甲操作完成再重复该步骤
当多个线程同时操作同一资源时,对资源的保护正确读写的操作,有synchronized代码块、synchronized方法
以卖3个窗口100张票为例
该方法尽量使用Runnable接口方式来实现
格式:
synchronized(同步监视器){
//同步监视器可以是任意类的对象,同一个对象同一把锁,处于同一个同步机制里
需要被同步的代码体
}
class MyThread implements Runnable {
private static int ticket = 100;
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
public class SynchronizedTest {
public static void main(String[] args) {
Thread m = new Thread(new MyThread());
m.start();
}
}
如果改用extends方式来实现就会出现问题,因为synchronized同步代码块中的同步监视器(锁)是this,即为当前对象,而extends方法需要调用3个对象,所以上了3把锁,此时跟没上锁的效果是一样的,很容易就同一张票卖了几次
class MyThread extends Thread{
private static int ticket = 100;
public MyThread(String name){
super(name);
}
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
public class SynchronizedTest{
public static void main(String[] args) {
MyThread m1 = new MyThread("窗口1");
m1.start();
MyThread m2 = new MyThread("窗口2");
m2.start();
MyThread m3 = new MyThread("窗口3");
m3.start();
}
}
改进后的extends方式就是直接声明一个静态的任意对象当作同步监视器
private static Object obj = new Object();
class MyThread extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
public MyThread(String name){
super(name);
}
public void run() {
while (true) {
if(ticket<10) {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// try {
// sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
}
此时的同步代码块包括了
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
如果把同步代码块缩小
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
if (ticket == 0) {
System.out.println("已售罄");
break;
}
synchronized (obj) {
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
运行后
原因:3个线程只要有一个已经经过了if判断,但卡在了synchronized之前,那么他就会导致ticket=0变成ticket=-1,此后再也不会经过if语句来break了,所以停不下来
if (ticket == 0) {
System.out.println("已售罄");
break;
}
但如果同步代码块框的太多,跟单线程没区别
public 返回值 synchronized 方法(){ } 相当于 public 返回值 方法(){ synchronized(this){ .................... } //同步方法相当于把整个方法用同步代码块框起来 }//并且以this为同步监视器
一般都是定义一个synchronized方法,把这个方法放在run里
效果和注意点跟同步代码块相同
public void run(){
System.out.println(Thread.currentThread().getName() + "窗口准备卖票");
show();
}
private synchronized void show(){
while(true){
if (ticket == 0) {
System.out.println("已售罄");
break;
}
System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
}
}
不同的线程分别需要占用对方需要的同步资源不放(不释放锁),都在等对方先放弃自己所需要的同步资源(锁),就形成的线程的死锁
出现死锁之后不会报错,也不会出现提示,所有线程都处于阻塞状态,无法继续
解决死锁的方法:
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
一个线程run中A锁套B锁,另一个线程run中B锁套A锁,一旦两个同时运行就容易出现死锁。尤其是设置一个sleep沉睡,死锁更容易出现
class Mythread implements Runnable{
public void run(){
......
synchronized(锁A){
//A锁套B锁
synchronized(锁B){
......
}
}
......
}
}
class Youthread implements Runnable{
public void run(){
......
synchronized(锁B){
//B锁套A锁
synchronized(锁A){
......
}
}
......
}
}
//主方法体中
new Thread(new Mythread()).start();
new Thread(new Youthread()).start();
ReentrantLock类的实例化有一个形参fair
private ReentrantLock lockkk = new ReentrantLock(true);叫公平锁
private ReentrantLock lockkk = new ReentrantLock();和
private ReentrantLock lockkk = new ReentrantLock(false);叫非公平锁
公平锁:多个共用同一把锁的线程有序排队,并按排队顺序上锁
private ReentrantLock lockkk = new ReentrantLock(true);//公平锁
。。。。。。。。。
public void run(){
for(int i = 1;i<=100;i++){
lockkk.lock();//上公平锁lockkk
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
//输出内容
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}//加上sleep效果更明显,防止因为屏幕输出时间差造成的误导
lockkk.unlock();//释放公平锁lockkk
//因为是公平锁,被阻塞的线程按顺序排队进入被lock和unlock方法夹在中间的代码块,
//此后该调用lock的线程进入排队的末尾,不与其他线程争抢资源
//配合上一个不短的sleep放大有序排队的效果,如果sleep值过小仍然可能冲突
}
}
非公平锁:多个共用同一把锁的线程有序排队,但不按顺序上锁,仍像synchronized一样靠抢
private ReentrantLock lockkk = new ReentrantLock(false);
private ReentrantLock lockkk = new ReentrantLock();
//两种表达都是表示非公平锁
public void run(){
for(int i = 1;i<=100;i++){
lockkk.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}//sleep也可以放在输出语句之前,只要是在lock和unlock之间就行了
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
lockkk.unlock();
}
}
上面的ReentrantLock类提供的lock公平锁可以实现多个线程按顺序每个都执行一次,当
线程数=2时,wait notify方法也能达到这样的效果
private static Object obj = new Object();
public void run(){
synchronized(obj)
for(int i = 1;i<=100;i++){
obj.notify();//唤醒另一个线程
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(Thread.currentThread().getName());
Depot(this.account,1000);
try {
obj.wait();//本线程阻塞,并释放同步监视器obj
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
当线程数>2时,即便是用notifyAll()也不能达到按顺序的效果,因为此时唤醒了其他线程之后,不像lock公平锁那样按顺序排队,notifyAll只管唤醒,线程资源的争夺仍然是随机的
- wait():阻塞自己,释放锁(同步监视器)。wait之后仍然可以有语句
- notify():唤醒一个线程,按优先级随机。一般放在代码块最开头
- notifyAll():唤醒其他所有线程,不按顺序
wait()、notify()、notifyAll():三个方法都是
- 写在同步方法or同步代码块中
- 需要用同步监视器(锁对象)来调用
- 因此定义在java.lang.Object中,因为需要所有对象都能调用
- 报错信息是IllegalMonitorStateException
wait会释放同步监视器,其他线程可以进入synchronized代码块内部并获取锁
sleep不会释放同步监视器,如果是同步线程,那么sleep会导致整个多线程阻塞
wait定义在Object类中,需要在同步代码块中用同步监视器对象调用,sleep是定义在Thread类中的静态方法,随时可以调用
为什么synchronized同步代码块既不能框多,也不能框少_古细亚沾点-CSDN博客
线程通信:生产者消费者源码与解析_古细亚沾点-CSDN博客