👀作者简介:大家好,我是@Starry。
🚩🚩 个人主页:@Starry
支持我:点赞+关注~不迷路🧡🧡🧡
✔系列专栏:javase高级⚡⚡⚡
(❁´◡`❁)励志格言:生活如波浪,有波谷也有波峰。在高峰的时候,且慢高歌,在波谷的时候且慢落泪;一浪翻一浪,一波过一波,便是彼岸🤞🤞
一.线程 进程 程序的理解
不知道友友有没有这样的疑问,程序员中的程序到底是什么意思?程序到底指的什么?程序是线程吗?接着带着这样的疑问,听我娓娓道来👼
1。程序:是为了完成某一目的,用某种语言编写的一组特殊指令的集合,即是一段静态代码。
(简单来说就是为了解决万恶的需求,程序员用一种语言,编写的一段能够发号命令的代码)是不是很简单丫😎~
2.进程:程序的一次执行过程,或是正在运行的一个程序。
说明:进程最为资源分配的单位,系统在运行时会为每一个进程分配不同的内存区域(这个我们下文在细细分析)。
( 其实就是在运行程序时,接受执行程序发出的一项任务。不知道大家有没有尝试过,我们在打开程序检测软件时,打开一个软件就会监测到一个程序的产生)。
3.线程:进程可以进一步细化为线程,线程是进程的一条执行路径(很容易想到一个进程可以有多个线程)
说明;线程作为执行和调度的单位,每个线程拥有独立的栈和程序计数器。线程切换的开销小
简要梳理一下他们之间的关系:
上面提到线程作为执行和调度的单位,每个线程拥有独立的栈和程序计数器。那么JVM是怎么分配的呢?为什么对于线程如此偏心?
每个线程:都有都有独立的栈和程序计数器
多个线程:共享同一个进程中方法区,堆。
其实仔细一看其实这并不是偏心与线程,完全是把活全让它自己干了,拿着自己的栈,和程序计数器工具,还要争抢公共资源。线程:真的栓Q~😑
二.并行与并发
在讲解并行与并发的时候首先要搞清楚一个点,单核cpu和多核cpu.
🐟:1.单核cpu顾名思义就是在一个时间单元内,只能执行一个线程的任务。但是单核cpu表面看起来是可以执行多个线程的,所以单核cpu是一种假的多线程,之所以额能够造成多线程的假象是因为cpu执行的频率是很高的。肉眼是无法察觉的。
这里我们举一个例子;
在一个饭店有很多顾客但是只有一个厨师,所以一个厨师要兼顾好多菜,由于厨师做菜的技术高超,动作熟练,在很快的时间内将所有的菜都按时端上了餐桌。表面上是完成了多个任务,但是其实只有一个人。当然真实的cpu的执行速度,肯定比图中我这个朋友单身二十年的手速快~🤣🤣。
2.如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
3 .一个Java应用程序java.exe,其实至少三个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程
理解了单核与多核cpu以后,并行与并发就好理解了
(1)并行:多个cpu执行多个任务(etc:多个人做不同的事)
(2)并发:单个cpu(采用时间片)同时实行多个任务(etc:妙杀,一个厨师做菜的实例)
三.创建多线程的两种基本方式
(1).继承Thread类的方式
实现步骤(创建一个线程遍历打印1-100的所有奇数)
- 创建一个类实现Thread类
- 在该类中重写run()方法–>实现打印奇数
- 创建该类的对象,利用对象调用start()方法(run()会被同时启动)
class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i%2!=0){
System.out.println(i);
}
}
}
}
public class BuildThread {
public static void main(String[] args) {
MyThread myThread=new MyThread();
myThread.start();
}
}
这里得出了start()函数的功能:
1.启动线程
2.调用start()方法
说明两个问题:
问题一:我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
🙀问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
(2)实现Runnable()接口的方法
- 常见一个类实现Runnable()接口
- 创建该类的对象
- 以该类的对象为参数创建Thread类的对象 - Thread类的对象调用start()方法
package Run;
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i+=2) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class ThreadTest2{
public static void main(String[] args) {
MyThread myThread=new MyThread();
Thread r1=new Thread(myThread);
r1.start();
}
}
两种方法的本质都是创建Thread的对象,调用start()方法
(3)两种方式之间对比
这里我们用一个实例对两种方法进行对比
实例:创建三个窗口同时卖100张票
方法一实现
错误实例
class Tickets extends Thread{
public int tickets=100;
@Override
public void run() {
while (tickets>0)
{
System.out.println(Thread.currentThread().getName()+"则会那个在卖第"+tickets+"张票");
tickets--;
}
}
}
public class OutPutTickets {
public static void main(String[] args) {
Tickets windows3=new Tickets();
Tickets windows1=new Tickets();
Tickets windows2=new Tickets();
windows1.start();
windows2.start();
windows3.start();
}
}
由上面的代码我们知道这肯动是错误的,由于一个Tickets对象只能调用start()所以我们这里创建了三个对象。让我们来看一下部分运行结果
由图中可以看出,每张票都几乎会有三张重复票。其实原因是,我们创建了三个对象,每个对象调用的时候都会有100票,这里需要将 tickets属性该成static对象,使他只具有一份。
class Tickets extends Thread{
public static int tickets=100;
@Override
public void run() {
while (tickets>0)
{
System.out.println(Thread.currentThread().getName()+"则会那个在卖第"+tickets+"张票");
tickets--;
}
}
}
public class OutPutTickets {
public static void main(String[] args) {
Tickets windows3=new Tickets();
Tickets windows1=new Tickets();
Tickets windows2=new Tickets();
windows1.start();
windows2.start();
windows3.start();
}
}
看一下结果截图:除了100外别的票号几乎没有重复票了(这里100重票是由线程安全造成的,这个问题我们下文在细细的研究)
方法二实现
class Tickets implements Runnable{
public int tickets=100;
@Override
public void run() {
while (tickets>0)
{
System.out.println(Thread.currentThread().getName()+"则会那个在卖第"+tickets+"张票");
tickets--;
}
}
}
public class OutPutTickets {
public static void main(String[] args) {
Tickets w=new Tickets();
Thread windows1=new Thread(w);
Thread windows2=new Thread(w);
Thread windows3=new Thread(w);
windows1.start();
windows2.start();
windows3.start();
}
}
由结果可知实现Runnable()接口的方式,Tickets对象自动就可以是一份,自然100张票也是三个窗口共用一份,不用加static。
两者方法对比概括
开发中一般使用实现Runnable()接口的方法,原因如下:
- 规避了java单继承的局限性
- 实现的方式更适合来处理多个线程共享数据的情况。
联系:public class extends Thread / implements Runnable
- 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
- 目前两种方式,要想启动线程,都是调用的Thread类中的start()。
四.Thread类中常用的方法
1.start():启动当前线程;调用当前线程的run()
2.run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3…currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():释放当前cpu的执行权
7.join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。
8.stop():已过时。当执行此方法时,强制结束当前线程。
9.sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。在指定的millitime毫秒时间内,当前线程是阻塞状态。
10.isAlive():判断当前线程是否存活
这里我们主要分析一下上面画线的部分的方法
(1)yield()用法
yield()释放当前cpu的执行权
请看下面的代码:
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if(i==20){
Thread.currentThread().yield();
}
}
}
}
public class BuildThread {
public static void main(String[] args) {
MyThread myThread=new MyThread();
Thread t=new Thread(myThread);
t.start();
for (int i = 0; i <100 ; i++) {
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
在线程Thread-0执行的过程中遍历到20时,释放cpu的执行权,此时cpu的执行权很有可能会被main线程获取。(注意这里只是说的有可能,因为这个方法释放cpu的执行权后,cpu可能还会把执行权又分配给该线程)
cpu执行权被main线程抢占
cpu执行权未被main线程抢占
(2)join()的用法
join()字面意思就是加入的意思。具体的原理是->join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态。请看下面的测试代码:
class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i <100 ; i++) {
if(i%2==0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class BuildThread {
public static void main(String[] args) throws InterruptedException {
MyThread myThread=new MyThread();
Thread t=new Thread(myThread);
t.start();
for (int i = 0; i <100 ; i++) {
if(i%2!=0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
if(i==21){
t.join();
}
}
}
}
一旦主线程遍历到了21。Thread0 线程join()进入了,此时主线程处于阻塞状态,一致等到Thread0线程执行完之后,主线程才继续执行。
让我们一起来看看代码的执行结果吧!
(3)线程优先级相关方法
线程的优先级有三个层次:最高级,最小级,默认优先级
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->默认优先级
如何获取和设置当前线程的优先级:
- getPriority():获取线程的优先级
- setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。
线程通信:wait() / notify() / notifyAll() :此三个方法定义在Object类中的。
五.线程的生命周期
线程相对人来说也是一个个体,也是要经历人生的不同阶段的。
一个线程要有五种状态:创建,就绪,运行,阻塞,死亡
让我们画一张图梳理一下吧!
六.线程的同步机制
(1).线程安全的单例模式
面试题:写一个单例模式(懒汉式):
我们先简单看一下单例模式的基本实现方式
public class Bank {
private static Bank instance=null;
public static Bank getInstance(){
if(instance==null)
{
instance=new Bank();
}
return instance;
}
}
很显然上面的代码是不安全的,那么何为安全?,上面的代码为什么就是不安全的呢?且看我下面这张图~
那么如何解决线程安全问题呢
(2)解决线程安全的两种方式
我们继续引用上文三个窗口买票的例子来对解决线程安全的问题做一个研究,上文提到,即使使用继承Thread类的方法实现买票,同时票数添加了static还是使用实现Runnable()接口的方法实现买票都依然会存在一定数目的重票.我们通过两种方法对这种线程安全的现象进行解决。
1.同步代码块
同步代码块的基本形势如下
synchronized(同步监视器){
//需要被同步的代码
}
我们具体说明一下代码中一些具体的含义
1.需要被同步的代码:操作共享数据的代码即为需要被同步的的代码。(这里的代码不能包含多了,也不能包含的少了)
2. 共享数据:多个线程共同操作的变量 etc票数tickets
3. 同步监视器:俗称就是“锁”,任何一个类的对象都可以充当锁。
注意:这里要求多个线程必须共用同一把锁。
我们试着用这种方法解决重票的问题
class MyThread1 extends Thread{
private static int tickets=100;
@Override
public void run() {
while(true){
synchronized (this.getClass()) {
if (tickets > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + ":" + "正在售卖第" + tickets + "张票");
tickets--;
} else {
break;
}
}
}
}
}
public class SellTickets {
public static void main(String[] args) {
MyThread1 windows1=new MyThread1();
MyThread1 windows2=new MyThread1();
MyThread1 windows3=new MyThread1();
windows1.start();
windows2.start();
windows3.start();
}
}
这里可以看到就不会有重票的问题了,然后我们尝试用这种方法解决单例模式的线程安全问题:
public class Bank {
private static Bank instance=null;
public static Bank getInstance() {
if (instance == null) {
synchronized (Bank.class){
if (instance == null) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Bank();
}
}
}
return instance;
}
}
2.同步方法
理解了上面这种方法,线程同步方法解决线程安全问题就十分好理解了,
就是将同步的数据抽调为一个方法,然后为这个方法加上锁。
我们用这种方法解决重票问题(将公共数据tickets抽调出来)
class MyThread1 extends Thread {
private static int tickets = 100;
@Override
public void run() {
while (true) {
check();
}
}
public static synchronized void check() {
if (tickets > 0) {
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "正在售卖第" + tickets + "张票");
tickets--;
}
}
}
public class SellTickets {
public static void main(String[] args) {
MyThread1 windows1=new MyThread1();
MyThread1 windows2=new MyThread1();
MyThread1 windows3=new MyThread1();
windows1.start();
windows2.start();
windows3.start();
}
}
我们用这种方法解决单例模式线程安全问题(整个方法本身就是操作共享数据的)
public class Bank {
private static Bank instance=null;
public synchronized static Bank getInstance() {
if (instance == null) {
instance = new Bank();
}
return instance;
}
}
(2)死锁问题(deadLock)
😎😎敲黑板:关于死锁问题发理解:
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
敲黑板:
1、当出现死锁问题的时候不会出现异常,也不会有提示,只是所有线程处于阻塞状态无法继续。
2.我们使用同步时要避免出现死锁
下面我们来看一下出现死锁的实例代码吧☟☟
//死锁的演示
class A {
public synchronized void foo(B b) { //同步监视器:A类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了A实例的foo方法"); // ①
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用B实例的last方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A类的对象:a
System.out.println("进入了A类的last方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 进入了B实例的bar方法"); // ②
try {
Thread.sleep(200);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("当前线程名: " + Thread.currentThread().getName()
+ " 企图调用A实例的last方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了B类的last方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
public void run() {
Thread.currentThread().setName("副线程");
// 调用b对象的bar方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
七.线程通信
线程通信重要涉及到三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
方法使用的注意事项:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中
可能通过叙述还不能完全理解,让我们用一个实例来练习一下
交替打印:使用两个线程印 打印 1-100 。线程1, 线程2 交替打印
/**
* 使用两个线程印 打印 1-100 。线程1, 线程2 交替打印
*/
class Print implements Runnable{
int target=1;
@Override
public void run() {
while(true) {
synchronized (this) {
//唤醒被wait()阻塞的一个线程,如果有很多线程就优先级高的进行释放
notify();
if (target <= 100) {
System.out.println(Thread.currentThread().getName() + " " + target);
target++;
try {
//wait()的特点,阻塞的同时还会释放锁
//wait()的调用者必须是同步代码块或者是同步方法中的同步监视器
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class Main {
public static void main(String[] args) {
Print p=new Print();
Thread t1=new Thread(p);
Thread t2=new Thread(p);
t1.start();
t2.start();
}
}
从上面的代码不难看出,t1和t2交替阻塞,并且交替相互唤醒,达到了两个数据交替打印1-100的数,让我们看一下运行结果吧!😁😁
线程通信经典面试题(sleep() 和 wait()的异同?)
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
八。JdK5.0新增的线程创建方式
(1)第三种创建线程的方法(实现Callable接口的方式)
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
-
- call()可以返回值的。
-
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
-
- Callable是支持泛型的
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
(2)第四中创建线程的方法(运用线程池创建)
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
好处:
- 1.提高响应速度(减少了创建新线程的时间)
- 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没任务时最多保持多长时间后会终止