黑马程序员——Java语言基础——04.多线程(1)多线程概念

来源:互联网 发布:ubuntu vi删除行 编辑:程序博客网 时间:2024/05/21 03:24

------- android培训、java培训、期待与您交流! ----------

本节考点:
一、线程、进程的理解
二、多线程的两种创建方式
三、线程的五种状态
新建:new一个Thread对象或者其子类对象就是创建一个线程,当一个线程对象被创建,但是没有开启,这个时候,
      只是对象线程对象开辟了内存空间和初始化数据。        
就绪:新建的对象调用start方法,就开启了线程,线程就到了就绪状态。
      在这个状态的线程对象,具有执行资格,没有执行权。
运行:当线程对象获取到了CPU的资源。
      在这个状态的线程对象,既有执行资格,也有执行权。
冻结:运行过程中的线程由于某些原因(比如wait,sleep),释放了执行资格和执行权。
      当然,他们可以回到运行状态。只不过,不是直接回到。
      而是先回到就绪状态。
死亡:当线程对象调用的run方法结束,或者直接调用stop方法,就让线程对象死亡,在内存中变成了垃圾。
四、sleep()和wait()的区别
(1)这两个方法来自不同的类,sleep()来自Thread类,和wait()来自Object类。
(2)sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,
要让b线程睡觉要在b的代码中调用sleep。而wait()是Object类的非静态方法
(3)sleep()释放资源不释放锁,而wait()释放资源释放锁;
(4)使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
五、产生死锁的原因。
六、同步,延迟加载的单例设计模式

1-1 多线程的概念

1-1-1 进程、线程、多进程的概念

进程:正在进行中的程序(直译)。
线程:进程中一个负责程序执行的控制单元(执行路径)。
P.S.
1、一个进程中可以有多个执行路径,称之为多线程。
2、一个进程中至少要有一个线程。
3、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
多线程的好处:解决了多部分代码同时运行的问题。
多线程的弊端:线程太多,会导致效率的降低。
其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。
示例:
JVM启动时启动了多条线程,至少有两个线程可以分析的出来:
①执行main函数的线程,该线程的任务代码都定义在main函数中。
②负责垃圾回收的线程。

1-1-2 创建线程方式一:继承Thread类

1、定义一个类继承Thread类。
2、覆盖Thread类中的run方法。
3、直接创建Thread的子类对象创建线程。
4、调用start方法开启线程并调用线程的任务run方法执行。
创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在了主函数中。
而自定义的线程,它的任务在哪儿呢?
Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。
开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。
示例:
<span style="font-family:Microsoft YaHei;font-size:14px;">class ThreadTest{//多线程演示,开启多线程的子类必须继承Thread类,开启线程必须使用start();方法//使用run时仅仅是普通的调用,不会开启多线程public static void main(String[] args){Demo d1 = new Demo("你好");//d1.setName("sha111----");d1.start();//d1.run();Demo d2 = new Demo("我好");d2.start();//d2.setName("sha2_____----");//d2.run();for (int i=0; i<50; i++){System.out.println(i+"nihaoassssss------");}}}class Demo extends Thread{private String name;Demo(String name){super(name);//给线程命名}public void run(){for (int i=0; i<50; i++){System.out.println(i+"大家好"+Thread.currentThread().getName());}}}</span>

1-1-3 创建线程方式二:实现Runnable接口

1、定义类实现Runnable接口。
2、覆盖接口中的run方法,将线程的任务代码封装到run方法中。
3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
     为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。
     所以要在线程对象创建时就必须明确要运行的任务。
4、调用线程对象的start方法开启线程。
实现Runnable接口的好处:
1、将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
2,避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。
示例:
<span style="font-family:Microsoft YaHei;font-size:14px;">class Ticket implements Runnable{//多个线程卖票,卖100张,不可以用for循环,因为每个循环中的ticket都不一样,必须用while。//这是第二种建立循环的方式:实现Runnable接口,再将建立后的对象传入Thread中。private int ticket = 100;public void run(){while(ticket>0){System.out.println(Thread.currentThread().getName()+"卖票"+ticket);ticket--;}}}class TicketTest{public static void main(String[] args){Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);Thread t4 = new Thread(t);t1.start();t2.start();t3.start();t4.start();}}</span>

为什么要有Runnable接口的出现?

1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。

可是该类中的还有部分代码需要被多个线程同时执行。这时怎么办呢?

只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。

所以,通常创建线程都用第二种方式。

因为实现Runnable接口可以避免单继承的局限性。

 

2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。

所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。

 

实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。

-------------------------------------------------------

//面试new Thread(new Runnable(){  //匿名public void run(){System.out.println("runnable run");}}){public void run(){System.out.println("subthread run");}}.start();  //结果:subthread run

-------------------------------------------------------

Try {

Thread.sleep(10);

}catch(InterruptedException e){}// 当刻意让线程稍微停一下,模拟cpu切换情况。

1-2 线程安全问题

1-2-1 线程安全问题产生的原因

需求:模拟4个线程同时卖100张票。
代码见上面

原因分析:
出现上图安全问题的原因在于Thread-0通过了if判断后,在执行到“num--”语句之前,num此时仍等于1。
CPU切换到Thread-1、Thread-2、Thread-3之后,这些线程依然可以通过if判断,从而执行“num--”的操作,因而出现了0、-1、-2的情况。
线程安全问题产生的原因:
1、多个线程在操作共享的数据。
2、操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

1-2-2 同步

思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。
必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。
在java中,用同步代码块就可以解决这个问题。
同步代码块的格式:
synchronized(对象){
     需要被同步的代码;
}

考点问题:请写一个延迟加载的单例模式?写懒汉式;当出现多线程访问时怎么解决?加同步,解决安全问题;效率高吗?不高;怎样解决?通过双重判断的形式解决。

class Single{private static Single s = null;private Single(){}public static Single getInstance(){ //锁是谁?字节码文件对象;if(s == null){synchronized(Single.class){if(s == null)s = new Single();}}return s;}}

同步的好处:解决了线程的安全问题。
同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步的前提:必须有多个线程并使用同一个锁。
同步函数
同步方法是指进入该方法时需要获取this对象的同步锁,在方法上使用synchronized关键字,
使用this对象作为锁,也就是使用了当前对象,因为锁住了方法,所以相对于代码块来说效率相对较低。
注:静态同步函数的锁是该方法所在的类的字节码文件对象,即类名.class文件
格式:
修饰词 synchronized 返回值类型 函数名(参数列表)
{
需同步的代码;
}
同步函数和同步代码块的区别:
1、同步函数的锁是固定的this。
2、同步代码块的锁是任意的对象。

建议使用同步代码块。

示例1:
public class ThreadTest {public static void main(String[] args) {Ticket t = new Ticket();Thread t1 = new Thread(t);Thread t2 = new Thread(t);Thread t3 = new Thread(t);t1.start();t2.start();t3.start();}}class Ticket implements Runnable {private static int tick = 100;private boolean flag = true;public void run(){while(flag){synchronized (Ticket.class) {if(tick>0)System.out.println(Thread.currentThread().getName()+"   "+tick--);elseflag = false;}}}}
示例2:
<span style="font-family:Microsoft YaHei;font-size:14px;">/** * 需求:两个用户向银行存钱,每人存三次 * @author user * */public class BankDemo {public static void main(String[] args) {ATM a = new ATM();new Thread(a).start();new Thread(a).start();}}class Bank{Object obj = new Object();private int sum = 0;public void add(int i) {synchronized (obj) {sum = sum + i;System.out.println(sum);}}}class ATM implements Runnable{private Bank b = new Bank();public void run(){for (int i=0; i<3; i++){b.add(100);}}}</span>

1-3 线程的状态

新建:new一个Thread对象或者其子类对象就是创建一个线程,当一个线程对象被创建,但是没有开启,这个时候,
      只是对象线程对象开辟了内存空间和初始化数据。        
就绪:新建的对象调用start方法,就开启了线程,线程就到了就绪状态。
      在这个状态的线程对象,具有执行资格,没有执行权。
运行:当线程对象获取到了CPU的资源。
      在这个状态的线程对象,既有执行资格,也有执行权。
冻结:运行过程中的线程由于某些原因(比如wait,sleep),释放了执行资格和执行权。
      当然,他们可以回到运行状态。只不过,不是直接回到。
      而是先回到就绪状态。
死亡:当线程对象调用的run方法结束,或者直接调用stop方法,就让线程对象死亡,在内存中变成了垃圾。
sleep()和wait()的区别:
(1)这两个方法来自不同的类,sleep()来自Thread类,和wait()来自Object类。
(2)sleep是Thread的静态类方法,谁调用的谁去睡觉,即使在a线程里调用了b的sleep方法,实际上还是a去睡觉,
要让b线程睡觉要在b的代码中调用sleep。而wait()是Object类的非静态方法
(3)sleep()释放资源不释放锁,而wait()释放资源释放锁;
(4)使用范围:wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用

1-4 死锁

两个线程对两个同步对象具有循环依赖时,就会发生死锁
例如:
class Test implements Runnable{private boolean flag;Test(boolean flag){this.flag = flag;}Object obj = new Object();public void run(){if(flag){while(true){synchronized(obj){System.out.println(Thread.currentThread().getName()+"...if locka ");synchronized(this){System.out.println(Thread.currentThread().getName()+"..if lockb");}}}}else{while(true){synchronized(this){System.out.println(Thread.currentThread().getName()+"..else lockb");synchronized(obj){System.out.println(Thread.currentThread().getName()+".....else locka");}}}}}}/*class MyLock{static Object locka = new Object();static Object lockb = new Object();}*/class  DeadLock{public static void main(String[] args) {Thread t1 = new Thread(new Test(true));Thread t2 = new Thread(new Test(false));t1.start();t2.start();}}
要避免这种情况。


------- android培训、java培训、期待与您交流! ----------


0 0