这两天学习多线程的基础(趁热总结一下)
一 多线程的概念:
1.1 什么是多线程?
在说线程之前不得不提的并发和并行的概念:
1.1.1并发和并行的概念:
- 并发:cpu在执行任务的时候是在同一时间段内cup来回执行这两个事件,也就是交替执行,因为速度很快,所以我们感觉是共同执行的。
- 并行:cpu在同一时间段内同时执行两个事件。注意是:同时执行。
1.1.2 线程和进程的概念:
- 进程的概念:
多为内存中运行的应用程序,而每个程序他是占用内存空间的,是需要在内存中运,一个程序可以是一个进程,也可以是多个进程,而一个进程包含一个线程叫单线程,包含多个线程叫多线程。运行程序的过程也是进程运行的过程。
- 线程的概念:
(首先我感觉在此之前肯定是多进程的东西,但是为什么会有多线程呢,因为凡是计算机,涉及到内存的东西,肯定是进行优化提高的,虽然在现在的电脑配置体现不大,肯在过去的程序编写的时候肯定要考虑程序的所需要的内存,一个程序的核心也就是算法,而算法是需要时间和空间的复杂度,相比之下,为了提高算法的效率,肯定是要优化程序 因为线程是需要占用内存的而频繁的占用内存是不可取的,所引出现了线程,线程是不用占内存空间的,线程是进程的儿子吧。)以上是我猜的
假如一个进程是一个应用程序,而这个应用程序可能有多个功能要求执行,而这些功能也就是线程的体现,线程没有自己独立的内存空间,他们利用进程共享的资源。进程作为程序分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位 ,一个线程的崩塌会导致一个进程的销毁,可能这就是他们说的线程不安全的地方,所以多进程的程序?会比多线程显得健壮。
1.1.3 一个线程的崩塌
(如果你和我一样是初学者,建议你最后再看)
-
代码如下:
public static void main(String[] args) { //创建lock锁(多太) Lock lock = new ReentrantLock(true);//公平锁 //利用匿名类对象创建线程 Runnable testRun = new Runnable() { @Override public void run() { lock.lock(); int[] arr = new int[3]; System.out.println(arr[3]); lock.unlock(); } }; //创建Thread类并行执行线程 Thread thread = new Thread(testRun); thread.start(); //创建第二个线程 new Thread(new Runnable() { @Override public void run() { lock.lock(); System.out.println("我是第二个线程"); lock.unlock(); } }).start();}
运行结果:
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 3
at threadcollapse.demo01$1.run(demo01.java:19)
at java.base/java.lang.Thread.run(Thread.java:844)
运行分析 根据结果我们可以看到第二个线程并没有执行,因为在第一个线程获得锁之后,然后程序发生了数组越界异常,而异常并没有捕捉,导致线程异常退出。重点是出现异常的线程一直占用lock这个锁,并且在获得锁之后没有释放前就抛出了异常,那么这个锁就永远在占用,那么第二个线程试图获得这个锁的时候,以为上一个没有释放,他就一直获取不到就一直阻塞。
1.2 如创建一个线程呢?
方法1: 声明一个类是Thread类的子类,应该重写Thread类的run方法,设置线程任务。
代码入下:
*
* 运行过程:两个线程并发的运行,(java的程序属于抢占性内存调度,那个线程的优先 级高,就执行那个线程,
* 如果是同一个线程,在没有设置线程的优先级的时候,默认都是5,然后两个线程线程同时抢占cpu的资源)
*当调用start方法的时候,两个线程(当前线程main主线程)和新创建的线程 并发的运行
*
*/
public class demo02 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();//调用该方法的start方法会开启新的线程,并执行线程的run方法
for (int i = 0; i <20 ; i++) {
System.out.println("爸爸程执行次序"+i);
}
}
}
public class MyThread extends Thread {//创建一个类继承了Thread类
//重写里面的run方法,并且设置线程任务。
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println("儿子执行次数"+i);
}
}
}
运行结果:
(可以看出是两个线程交替运行的)
创建一个类,并且继承Thread类,然后重写里面run方法,(而run方法里面实现线程的具体操作),之后创建一个实现类对象,然后调用了里面start方法,执行线程。要注意的是:线程的start方法只能运行一次,多次调用线程的start方法会报错,当调用start方法的时候,会把线程添加到线程组中,等待线程调度器调用,当获取资源的时候,就会进入运行状态
1.2多线程的内存图
(根据上面的代码相应的内存图)
图片如下:
二 两种创建线程的方法(Thread类,Runnable接口)
2.1 thread类的一些常用的方法:
- public String getName() :获取当前线程名称。
- public void start() :导致此线程开始执行,Java虚拟机调用此线程的run方法
- public void run() :此线程要执行的任务在此处定义代码。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停。
- Thread currentThread() :返回对当前正在执行的线程对象的引用。
- void setName(String name) 改变该线程的名字等于参数
代码如下:
2.1.1 获取线程名:
//getName方法
public static void main(String[] args) {
myThread myThread = new myThread();
myThread.start();
}
public class myThread extends Thread{
@Override
public void run() {
String name = getName();//通过getname获得线程的名称,返回值是一个string类型
System.out.println(name);
}
}
代码运行效果:
Thread-0
//直接调用获取线程名很好理解。
*******************华丽的分割线************************
//通过currenthread() 获得当前正在执行的线程。
代码如下:
public static void main(String[] args) {
myThread myThread = new myThread();
myThread.start();
System.out.println(Thread.currentThread().getName());//我这里直接写一起了
}
public void run() {
Thread thread = Thread.currentThread();//通过调用currenthread方法,返回一个thread对象
String name = thread.getName();//然后再调用.getname方法
System.out.println(name);
}
//这两种方法都就可以获得线程的名称。
运行效果:
main
Thread-0
2.1.2 设置线程的名称
两种方式修改线程的名称
1.获得main方法创建Thread的实现类之后,然后通过实现类.setName("")的形式设置线程的名称。
代码如下:
myThread myThread = new myThread();
myThread.setName("张博阳");
myThread.start();
2.利用构造方法,创建Thread实现类的构造方法,直接super调用父类的方法,利用父类的方式在创建线程的时候直接起名。
代码如下:
public class myThread extends Thread{
public myThread() {
}
public myThread(String name) {
super(name);//直接让父类进行处理
}
}
public static void main(String[] args) {
myThread threadName= new myThread("海绵宝宝");//在创建的时候直接传递参数
threadName.start();
}
2.1.3 sleep方法
sleep(long millis) ,当前正在执行的线程休眠(暂停执行)为指定的毫秒数,根据精度和系统定时器和调度的准确性。 什么意思呢,方法的功能是让线程等待几秒钟。
代码如下:
public class myThread2 extends Thread {
@Override
public void run() {
for (int i = 60; i >0; i--) {
try {
Thread.sleep(1000);//因为是继承Thread,父类没有抛出异常,这里也不能抛,只能处理
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("毫秒倒计时"+i+"秒");
}
}
}
public static void main(String[] args) {
myThread2 myThread2 = new myThread2();
myThread2.start();
}
运行效果:
毫秒倒计时60秒
毫秒倒计时59秒
毫秒倒计时58秒
毫秒倒计时57秒
.....
该方法让线程一秒钟休眠一次,实现了一个毫秒倒计时的功能。
2.2 Runable接口(创建线程的第二种方式)
2.2.1 Runable接口的步骤:
- 定义Runnable 接口的实现类,然后重写里面的run方法,同Thread的run方法相同,也是设置线程任务。
- 创建Runnable的实现类,(因为runnable接口中没有start方法)需要我们创建一个Thread类然后调用start方法开启线程.在Thread类的构造方法里面可以传递runnable 例如:thread(Runnable target) 分配一个新的 Thread对象。
- 开启多线程。
- 创建一个runnable接口的实现类
- 重写里面的run方法,设置线程任务
- 创建一个runnbale接口实现类对象
- 创建一个Thread类对象,构造方法中传递Runnable对象参数
- 执行Thread对象的start方法,开启多线程
代码如下:
//创建一个Runable接口的实现类
public class RunnbaleImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("新的线程执行"+i);
}
}
}
//主方法
public static void main(String[] args) {
RunnbaleImpl runnbale = new RunnbaleImpl(); //创建一个runnbale接口实现类对象
Thread thread = new Thread(runnbale); //创建一个Thread类对象,构造方法中传递Runnable对象参数
thread.start(); //执行Thread对象的start方法,开启多线程
for (int i = 0; i < 20; i++) {
System.out.println("主方法执行"+i);
}
}
运行效果:
主方法执行0
新的线程执行0
主方法执行1
新的线程执行1
主方法执行2
新的线程执行2
主方法执行3
新的线程执行3
主方法执行4
新的线程执行4
主方法执行5
新的线程执行5
......
2.2.2Thread类和Runnbale接口两者的区别:
- 避免了继承的局限性: Thread类是继承关系,在java中是单继承关系,一个类只能继承一个父类,这一点就有了局限性,而一个类可以实现多个接口,比较灵活,具有扩展性。
- 降低了程序的耦合性: Thread类是线程任务的设置和开启线程任务放在一起,具有耦合性。而Runable接口只是负责重写了里面run线程任务方法,开启新的线程任务还是由Thread方法执行,起到了解耦的作用。
- Runable接口适合多个线程共同访问共享一个资源。
- 在线程池中只能放Thread类的线程,不能放Thread类
2.2.3 利用匿名内部类的方式创建创建线程
- 什么是匿名内部类? 我的理解,匿名匿名就是没有名字的类,因为没有名字所以只能使用一次,作用就是简化代码的书写。
- 匿名内部里类的使用条件:必须要继承一个父类或者是一个接口的实现类,而这一点线程就都沾上了,方式1,你得继承Thread类,方式2 你的是Runnable接口的实现类,匿名类大大简化线程代码的编写。
代码入下:
public static void main(String[] args) {
show01();
show02();
}
private static void show02() {
//也是利用匿名内部类的方式创建线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("我是实现的Runnable接口的线程"+Thread.currentThread().getName());
}
}).start();// 我这里简化写一起了,new Thread(xxx).start();
}
private static void show01() {
//利用匿名内部类的方式创建线程Thread
new Thread(){
@Override
public void run() {
System.out.println("我是继承了Thread类的线程"+Thread.currentThread().getName());
}
}.start();
}
线程安全问题
由上面的继承Runnable接口我们得知可以访问并且共享同一个资源,但是在没有规定的情况下,多个线程访问资源的时肯能会产生线程问题。
问题:假如你要和你的女朋友去看哪吒(最近很火的)。
用美团订票的时候买的是7排的4,5号座位(我一般都是在后面的),但是你去了发现你的座位有人而且票的位置和场次都是和你一样的,这时候怎么版办?
什么意思呢? 就是三个顾客都通过手机去买共同的一百张电影票,由于共同访问共享资源然后就出现了线程安全问题。 代码如下:
int ticket = 100;//假如这场带电影做100个人
@Override
public void run() {
//为了保证一直出票
for (;;) {
if(ticket > 0) {
try {
Thread.sleep(20);//网络延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出第"+ticket+"张票");
ticket --;//相应的票数减少
}
}
public static void main(String[] args) {
//创建线程的实现类
TicketImpl ticket = new TicketImpl();
//假如同时三个想买票
Thread customer1 = new Thread(ticket);
customer1.setName("1号顾客");
Thread customer2 = new Thread(ticket);
customer2.setName("2号顾客");
Thread customer3 = new Thread(ticket);
customer3.setName("3号顾客");
customer1.start();
customer2.start();
customer3.start();
}
运行效果:
2号顾客正在出第100张票
1号顾客正在出第100张票
3号顾客正在出第100张票
2号顾客正在出第97张票
1号顾客正在出第97张票
3号顾客正在出第97张票
3号顾客正在出第94张票
.....
3号顾客正在出第4张票
1号顾客正在出第2张票
3号顾客正在出第1张票
2号顾客正在出第0张票
1号顾客正在出第-1张票
不光出现了重复的票数,还出现了不存在的票数。 原因如下:
感觉写的有点长:分开写,预知后事如何,请看下回分解。
文章在写笔记的,(写的和我的日记差不多)中间查看了不少的博客以下是我引用的博客:
线程死掉之后的结果:
一个线程崩溃了,线程所在的进程是不是就要崩溃?