02 如何创建线程 线程并发与synchornized

时间:2022-06-24 18:36:59

所有程序运行结果 请自行得出

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

步骤:

1,定义一个类继承Thread类。

2,覆盖Thread类中的run方法。

3,直接创建Thread的子类对象创建线程。

4,调用start方法开启线程并调用线程的任务run方法执行。

 

 1 /*
 2  * 需求:我们要实现多线程的程序。
 3  * 如何实现呢?
 4  *     由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
 5  *     而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
 6  *     Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
 7  *     但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。
 8  *     由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
 9  *     然后提供一些类供我们使用。我们就可以实现多线程程序了。
10 
11  * 那么Java提供的类是什么呢?
12  *         Thread
13  *         通过查看API,我们知道了有2中方式实现多线程程序。
14  
15  * 方式1:继承Thread类。
16  * 步骤
17  *     A:自定义类MyThread继承Thread类。
18  *     B:MyThread类里面重写run()?
19  *     C:创建对象 new
20  *     D:启动线程 start
21  */
22 
23 
24 /*
25  * 该类要重写run()方法,为什么呢?
26  * 不是类中的所有代码都需要被线程执行的。
27  * 而这个时候,为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。
28  */
29 
30 public class MyThread extends Thread {
31     @Override
32     public void run() {
33         // 自己写代码
34         // 一般来说,被线程执行的代码肯定是比较耗时的。所以我们用循环
35         for (int x = 0; x < 200; x++) {
36             System.out.println(x);
37         }
38     }
39 }

若执行以下代码:

MyThread my = new MyThread();
my.run();
my.run();

观察结果:程序是顺序执行的,并没有交替执行的现象的,为什么?

答:调用run()方法是单线程的。

      因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果

要想看到多线程的效果,就必须说说另一个方法:start()

MyThread my = new MyThread();

my.start();

 

面试题:run()和start()的区别?

run():仅仅是封装被线程执行的代码,直接调用是普通方法

start():首先启动了线程,然后再由jvm去调用该线程的run()方法

 

由于只start()了一次,观察不到想要的结果,那么就会想出如下的代码:

MyThread my = new MyThread();

my.start();

my.start();

运行后会发现抛出错误: IllegalThreadStateException:非法的线程状态异常

为什么呢?

因为start()2次相当于是my线程被调用了两次,而不是两个线程启动。

一个线程只能调用一次

要想观察到多线程的效果,正确的代码是:

1 MyThread my1 = new MyThread();
2 MyThread my2 = new MyThread();
3 my1.start();
4 my2.start();

运行后就能够看见效果了。

 

1.1线程名字

通过运行结果我们可以发现:我们并不知道谁是谁? 即哪个线程在执行

如何获取线程的名称呢?  getName()方法

 1 public class MyThread extends Thread {
 2     public void run() {
 3        for (int x = 0; x < 100; x++) {
 4            System.out.println(getName() + ":" + x);
 5        }
 6     }
 7 }
 8 
 9 MyThread my1 = new MyThread();
10 MyThread my2 = new MyThread();
11 my1.start();
12 my2.start();

发现:可以通过Thread的getName获取线程的名称 Thread-编号(从0开始)

名称为什么是:Thread-? 编号

下面看一下Thread的代码:

 1 class MyThread extends Thread {
 2     public MyThread() {
 3         super();
 4     }
 5 }
 6 
 7 class Thread {
 8     private char name[];
 9 
10     public Thread() {
11         init(null, null, "Thread-" + nextThreadNum(), 0);
12     }
13     
14     private void init(ThreadGroup g, Runnable target, String name,long stackSize) {
15         init(g, target, name, stackSize, null);
16     }
17     
18      private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
19         //大部分代码被省略了
20         this.name = name.toCharArray();
21     }
22     
23     public final void setName(String name) {
24         this.name = name.toCharArray();
25     }
26     
27     private static int threadInitNumber; //0,1,2
28     private static synchronized int nextThreadNum() {
29         return threadInitNumber++; //return 0,1
30     }
31     
32     public final String getName() {
33         return String.valueOf(name);
34     }
35 }

由原码可知:Thread通过一个字符数组name表示线程名称,用threadInitNumber变量来记录线程的编号

                每当一个线程调用时,threadInitNumber++以达到计数的目的,然后 "Thread-" + threadInitNumber转成字符数组给name

                调用getName()是将name转换为string,这就是我们看到的效果。

但默认的方法对我们来说往往没有意义。但我们可以通过setName(String name)来设置我们想要的名字

 1 我们还可以通过有参数的构造方法来命名线程
 2 public class MyThread extends Thread {
 3     public MyThread() {
 4     }
 5     
 6     public MyThread(String name){
 7         super(name);
 8     }
 9 
10     @Override
11     public void run() {
12         for (int x = 0; x < 100; x++) {
13             System.out.println(getName() + ":" + x);
14         }
15     }
16 }
17 //带参构造方法给线程起名字
18 MyThread my1 = new MyThread("林青霞");
19 MyThread my2 = new MyThread("刘意");
20 my1.start();
21 my2.start();

 

主线程的名字就是main 我们如何获取呢?

遇到这种情况,Thread类提供了一个很好玩的方法: public static Thread currentThread():返回当前正在执行的线程对象

在main方法中使用下面语句就可以得到多线程的名称 System.out.println(Thread.currentThread().getName());

 

1.2线程调度优先级

 

假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,

线程只有得到 CPU时间片,也就是使用权,才可以执行指令。

那么Java是如何对线程进行调用的呢?

 

线程有两种调度模型:

  分时调度模型     所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

  抢占式调度模型   优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

 

 Java使用的是抢占式调度模型:

static int MAX_PRIORITY 线程可以具有的最高优先级    10

static int MIN_PRIORITY  线程可以具有的最低优先级   1

static int NORM_PRIORITY 分配给线程的默认优先级     5

 

public final void setPriority(int newPriority)更改线程的优先级

public final int getPriority()返回线程的优先级

 

 * 注意:

线程默认优先级是NORM_PRIORITY = 5

线程优先级的范围是:1-10

线程优先级高仅仅表示线程获取的 CPU时间片的几率高(并非高优先级的线程一定每次都优先),

但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。

 1 public class ThreadPriority extends Thread {
 2 
 3     public void run() {
 4 
 5        for (int x = 0; x < 100; x++) {
 6            System.out.println(getName() + ":" + x);
 7        }
 8     }
 9 }
10 
11 ThreadPriority tp1 = new ThreadPriority();
12 ThreadPriority tp2 = new ThreadPriority();
13 ThreadPriority tp3 = new ThreadPriority();
14 
15  
16 tp1.setName("东方不败");
17 tp2.setName("岳不群");
18 tp3.setName("林平之");
19 
20  
21 // 获取默认优先级
22 System.out.println(tp1.getPriority());
23 System.out.println(tp2.getPriority());
24 System.out.println(tp3.getPriority());
25 
28 //设置线程优先级
29 tp1.setPriority(10);
30 tp2.setPriority(1);
31 
34 tp1.start();
35 tp2.start();
36 tp3.start();
37 

 

如果设置线程优先级: tp1.setPriority(100000);

报错:

IllegalArgumentException:非法参数异常。

抛出的异常表明向方法传递了一个不合法或不正确的参数。

 

1.3 sleep

 线程休眠  public static void sleep(long millis)

  Thread.sleep(1000); //1秒

  在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),

  此操作受到系统计时器和调度程序精度和准确性的影响。

  该线程不丢失任何监视器的所属权时间到后该线程继续执行 即不会执行的权利 时间到后继续往下走

我们可以通过以下代码感受:

 1 public class ThreadSleep extends Thread {
 2     @Override
 3     public void run() {
 4         for (int x = 0; x < 100; x++) {
 5             System.out.println(getName() + ":" + x + ",日期:" + new Date());
 6             try {
 7                 Thread.sleep(1000);
 8             } catch (InterruptedException e) {
 9                 e.printStackTrace();
10             }
11         }
12     }
13 }
14 public class ThreadSleepDemo {
15     public static void main(String[] args) {
16         ThreadSleep ts1 = new ThreadSleep();
17         ThreadSleep ts2 = new ThreadSleep();
18         ThreadSleep ts3 = new ThreadSleep();
19 
20         ts1.setName("林青霞");
21         ts2.setName("林志玲");
22         ts3.setName("林志颖");
23 
24         ts1.start();
25         ts2.start();
26         ts3.start();
27     }
28 }

 

创建线程的第二种方式:实现Runnable接口

1,定义类实现Runnable接口。

2,覆盖接口中的run方法,将线程的任务代码封装到run方法中。

3,通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。

    为什么?

       为线程的任务都封装在Runnable接口子类对象的run方法中。

      所以要在线程对象创建时就必须明确要运行的任务。

4,调用线程对象的start方法开启线程。

 

实现Runnable接口的好处:

1,将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务的封装成对象。

2,避免了java单继承的局限性。

所以,创建线程的第二种方式较为常用。

实例

 1 /*
 2  * 方式2:实现Runnable接口
 3  * 步骤:
 4  *         A:自定义类MyRunnable实现Runnable接口
 5  *         B:重写run()方法
 6  *         C:创建MyRunnable类的对象
 7  *         D:创建Thread类的对象,并把C步骤的对象作为构造参数传递
 8  */
 9 
10 public class MyRunnable implements Runnable {
11 
12     @Override
13     public void run() {
14         for (int x = 0; x < 100; x++) {
15             // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用
16             System.out.println(Thread.currentThread().getName() + ":" + x);
17         }
18     }
19 
20 }
21 
22 使用:
23 MyRunnable my = new MyRunnable();
24 // 创建Thread类的对象,并把C步骤的对象作为构造参数传递 Thread(Runnable target)
25 Thread t1 = new Thread(my);
26 Thread t2 = new Thread(my);
27 t1.setName("林青霞");
28 t2.setName("刘意");
29 t1.start();
30 t2.start();
31 
32 MyRunnable my = new MyRunnable();
33 // Thread(Runnable target, String name)
34 Thread t1 = new Thread(my, "林青霞");
35 Thread t2 = new Thread(my, "刘意");
36 t1.start();
37 t2.start();

2.1比较

02 如何创建线程 线程并发与synchornized

2.2 实现Runnable的最大好处就是可以实现共享同一资源

案例2:经典的买票问题

某电影院目前正在上映贺岁大片(红高粱,少林寺传奇藏经阁),

共有100张票,而它有3个售票窗口售票,请设计一个程序模拟该电影院售票。

 

先用Thread实现

public class SellTicket extends Thread {

    // 定义100张票
    // private int tickets = 100;
    // 为了让多个线程对象共享这100张票,我们其实应该用静态修饰
    private static int tickets = 100;

    public void run() {

       // 定义100张票
       // 每个线程进来都会走这里,这样的话,每个线程对象相当于买的是自己的那100张票,这不合理,所以应该定义到外面
       // int tickets = 100;
       // 是为了模拟一直有票
       while (true) {
           if (tickets > 0) {
              System.out.println(getName() + "正在出售第" + (tickets--) + "张票");
           }
       }
    }
}

public class SellTicketDemo {

    public static void main(String[] args) {

       // 创建三个线程对象
       SellTicket st1 = new SellTicket();
       SellTicket st2 = new SellTicket();
       SellTicket st3 = new SellTicket();

       // 给线程对象起名字
       st1.setName("窗口1");
       st2.setName("窗口2");
       st3.setName("窗口3");

       // 启动线程
       st1.start();
       st2.start();
       st3.start();
    }
}

 

现在用Runable接口:

 1 public class SellTicket implements Runnable {
 2     // 定义100张票
 3     private int tickets = 100;
 4     public void run() {
 5        while (true) {
 6            if (tickets > 0) {
 7 
 8               try{Thread.sleep(100)}catch(Exception e){}
 9               
10               System.out.println(Thread.currentThread().getName() + "正在出售第" + (tickets--) + "张票");
11            }
12        }
13     }
14 }

new Thread(new SellTicket()).start();
new Thread(new SellTicket()).start();
new Thread(new SellTicket()).start();

 

我们每次卖票都延迟100毫秒,此时你就会出现问题

原因就是因为多个线程并发导致了共享数据的安全问题

 

如何解决线程安全问题呢?

 要想解决问题,就要知道哪些原因会导致出问题:(而且这些原因也是以后我们判断一个程序是否会有线程安全问题的标准)

 A:是否是多线程环境

 B:是否有共享数据

 C:是否有多条语句操作共享数据

我们来回想一下我们的程序有没有上面的问题呢?

A:是否是多线程环境  是

B:是否有共享数据   是

C:是否有多条语句操作共享数据   是

 

由此可见我们的程序出现问题是正常的,因为它满足出问题的条件。

接下来才是我们要想想如何解决问题呢?

A和B的问题我们改变不了,我们只能想办法去把C改变一下。

 

Solution:

 基本上所有的并发模式在解决线程安全问题时,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称作同步互斥访问

 通常来说,是在访问临界资源前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。

   解决思就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程时不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。 

 

Java提供的解决方案

思想:

把多条语句操作共享数据的代码给包成一个整体,让某个线程在执行的时候,别人不能来执行。

问题是我们不知道怎么包啊?Java给我们提供了:同步机制。

 

解决思就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,

其他线程时不可以参与运算的。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

在java中,用同步代码块就可以解决这个问题。

 

同步代码块的格式:

synchronized(对象)

{

    需要被同步的代码

}

 

A:对象是什么呢?

    我们可以随便创建一个对象试试。(对象其实可以是任意类型的,但必须是同一个对象)

       synchronized(new Object()){}  No

       synchronized(object){}         Yes  可以使用 class文件

 

B:需要同步的代码是哪些呢?

  把多条语句操作共享数据的代码的部分给包起来

 

注意:

同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。

多个线程必须是同一把锁。

 

同步的好处:解决了线程的安全问题。

同步的弊端:相对降低了效率,因为同步外的线程的都会判断同步锁。

同步的前提:同步中必须有多个线程并使用同一个锁

 

l  synchronized 代码块

 synchronized(...){
   ..... } 
卖票问题的解决:
 1 public class ThreadDemo03 {
 2     public static void main(String[] args) 
 3     {
 4         Ticket t = new Ticket();//创建一个线程任务对象  此时只有一个Ticket对象  且只能有一个
 5 
 6         Thread t1 = new Thread(t);
 7         Thread t2 = new Thread(t);
 8         Thread t3 = new Thread(t);
 9         Thread t4 = new Thread(t);
10 
11         t1.start();
12         t2.start();
13         t3.start();
14         t4.start();
15     }
16 }
17 
18 class Ticket implements Runnable{
19     private  int num = 100;    //共享资源 必须放在这里 而不能在run()中
20     Object obj = new Object();//对象锁只能有一个
21 
22     public void run()
23     {
24         while(true)
25         {
26             synchronized(obj)
27             {
28                 if(num>0)
29                 {
30                     try{Thread.sleep(1000);}catch (InterruptedException e){}
31                     System.out.println(Thread.currentThread().getName()+".....sale...."+num--);
32                 }
33             }
34         }
35     }
36 }

l  synchronized 方法:

public synchronized type function (... ...); 
 1 private static synchronized void sellTicket() {
 2         if (tickets > 0) {
 3         try {
 4                 Thread.sleep(100);
 5         } catch (InterruptedException e) {
 6                 e.printStackTrace();
 7         }
 8         System.out.println(Thread.currentThread().getName()
 9                     + "正在出售第" + (tickets--) + "张票 ");
10         }
11 }

 

l  当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?

   对象的synchronized方法不能进入了,但它的其他非synchronized方法还是可以访问的

 1 public class TT implements Runnable {
 2 
 3      int b = 100;
 4 
 5      public synchronized void m1(){
 6             b = 10000;
 7             try {
 8                 Thread. sleep(5000);
 9                 System. out.println( "m1----b="+ b);
10            } catch (InterruptedException e) {
11                 e.printStackTrace();
12            }
13      }
14 
15      public void m2(){
16            System. out.println( "m2====="+ b);
17      }
18 
19      @Override
20      public void run() {
21            m1();
22      }
23 
24      public static void main(String[] args) {
25            TT tt = new TT();
26            Thread t = new Thread(tt);
27            t.start();
28 
29             try {
30                 Thread. sleep(1000);
31            } catch (InterruptedException e) {
32                 e.printStackTrace();
33            }
34 
35            tt.m2();
36      }
37 
38 }

 

锁对象是谁?

 * A:同步代码块的锁对象是谁呢?

 *     任意对象。

 

 * B:同步方法的格式及锁对象问题?

 *     把同步关键字加在方法上。

 *     同步方法是谁呢?

 *         this

 

 * C:静态方法及锁对象问题?

 *     静态方法的锁对象是谁呢?

 *     类的字节码文件对象。(反射会讲)

 

 

线程安全的类

StringBuffer sb = new StringBuffer();

Vector<String> v = new Vector<String>();

Hashtable<String, String> h = new Hashtable<String, String>();

Vector是线程安全的时候才去考虑使用的,但是即使要安全,我也不用你  

那么到底用谁呢?public static <T> List<T> synchronizedList(List<T> list)

List<String> list1 = new ArrayList<String>();// 线程不安全

List<String> list2 = Collections.synchronizedList(new ArrayList<String>()); // 线程安全

 

synchornized

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

  2)线程执行发生异常,此时JVM会让线程自动释放锁。

 

原理示意图

02 如何创建线程 线程并发与synchornized

jdk1.5还提供了lock锁 后面再说