Java多线程学习记录(一)

1.认识多线程

2.如何去创建多线程并且启动

3.多线程的属性和方法

4.线程在执行时会出现的状态

5.多线程带来的风险(为什么会出现线程不安全的情况)


(一)认识多线程    

     1)线程是什么: 

       在学习多线程之前,我们通常写的都是进程,把一个进程当作一个“执行流”,但是在学习多线程后,会发现线程也是⼀个"执⾏流".每个线程之间都可以按照顺序执行自己的代码.多个线程之间同时执行着多份代码。

     2)为什么我们需要线程:

      1.因为我们的cpu资源是有限的,而一个进程会占用一整个cpu核心的资源,所以我们通过线程,并发编程的来更合理运用cpu核心资源

      2.虽然多线程也能并发编程,但是线程更加的轻量,他的创建,销毁,调度都比进程更快更便捷,消耗的资源也更少。

        但是现在我们仍然嫌线程的频繁创建和销毁开销较大,所以有了协程(也就是轻量级的线程),线程池的出现。

      3)线程和进程的关系:

        首先我们要明确进程是包含线程的,每一个进程中都必须要有一个主线程存在

        其次要知道线程是调度执行的最小单位,进程是系统分配资源的最小单位。进程与进程间不共用同一块资源,而同一个进程中的线程共用这个进程所申请的空间

        进程与进程之间的关系不大,一个进程挂了另一个进程照样可以执行,而同一个进程中的一个线程挂了,可能会导致整个进程挂掉。

(二)如何去创建多线程并且启动

创建多线程的方法有很多比如

1.继承thread重写run方法
public class text {
     private  static  class MyThread extends Thread{
         @Override

         public void run() {
             while(true){
                 System.out.printf("这是一个线程");
             }
         }
     }

    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        MyThread myThread2=new MyThread();
        myThread.start();
        myThread2.start();
    }
}

        这里的每一个线程都是一个执行流,在之前,一个进程中如果有一个无限循环的while那么就不会执行到之后的方法,而用多线程是并发执行的,并不会影响其他线程。

 2.实现Runnable接口并且重写run方法
public class text {
     private  static  class MyRunnable implements Runnable{
         @Override

         public void run() {
             while(true){
                 System.out.printf("这是一个线程");
             }
         }
     }

    public static void main(String[] args) {
       Thread t1=new Thread(new MyRunnable());
              t1.start();
    }
}

      创建Thread类实例,调⽤Thread的构造⽅法时将1Runnable对象作为target参数.

对比上面两种方法:

 • 继承Thread类,直接使用this就表示当前线程对象的引用.

 • 实现Runnable接,this表示的是MyRunnable 的引用.需要使用 Thread.currentThread()

3.我们可以使用匿名内部类的方式来创建线程(此处省略)
4.lambda表达式
public class text {


    public static void main(String[] args) {
       Thread t1=new Thread(()->{
           System.out.printf("这也是个线程");
       });
       t1.start();
    }
}

     这里我们需要注意lambda表达式会涉及到一个叫变量捕获的东西,就是在lambda表达式里的变量必须是用final修饰的(不可变的)或者没有发生改变的量-------实时final

    

     在说完如何创建多线程后有个问题,之前我在网上看到有人问run 和 start 的区别,在我看来这两个没有什么联系。如果我们在main方法中调用了重写后的run方法,run方法里的方法体能够被执行出来,但是不是以多线程的方式执行,只有调用了start方法,我们才真正创建了一个线程。

     我们用jconsole可以看到在我们调用run方法是显示的是new,而在调用start时是runnable,那我们接下来就要说了什么是new,什么是runnable了----线程执行时的状态

(三)多线程的属性和方法

多线程的几个常见属性

       在我们创建了一个线程之后,系统会给我们线程自动分配一个id和name,尤其是id,id是每个线程所独有的,不同线程之间不会重复,而名字我们可以手动做修改,在jconsole中我们可以看到线程的名字,当然也包含其他属性。

       状态我们等会说,先说优先级,目前我没有发现这个优先级有什么用,看网上的文献说,优先级高的线程更容易被cpu调度执行,这里我还是要打上一个问号,因为多线程是随即调度抢占式执行的。

      后台线程指的是,如果这个线程没有结束也不会影响进程的结束,反之只要有一个非后台线程没有结束,这个进程就不会结束。

      是否存活也可以很简单的理解为,这个线程的run方法是否执行完了。

       中断线程(我认为中断线程有一些难度,所以在这里着重写一下):

       我们有两个方式可以中断线程

1.设定一个标志位,我们可以在外面修改

2.使用interrupt()方法来通知

示例1  通过设定标志位来中断线程(标志位要用volatile修饰)此关键字之后说

private static class MyRunnable implements Runnable {
 public volatile boolean isQuit = false;
 @Override
 public void run() {
 while (!isQuit) {
 System.out.println(Thread.currentThread().getName()
 + ": 别管我,我忙着转账呢!");
 try {
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 System.out.println(Thread.currentThread().getName()
 + ": 啊!险些误了⼤事");
    }
 }
 public static void main(String[] args) throws InterruptedException {
 MyRunnable target = new MyRunnable();
 Thread thread = new Thread(target, "李四");
 System.out.println(Thread.currentThread().getName()+ ": 让李四开始转账。");
 thread.start();
 Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
 + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
 target.isQuit = true;
    }
} 

示例2使用Thread.interrupted或者 Thread.currenthread.interrupted来代替自定义标志位

public class text {


    public static void main(String[] args) throws InterruptedException {
       Thread t1=new Thread(()->{
           while(!Thread.interrupted())
           System.out.printf("sos!!");
       });
        t1.start();
        Thread.sleep(1000);
        System.out.printf("我来救你了");
        t1.interrupt();
    }
}

thread收到通知的方式有两种

1)被wait/sleep/join等方法阻塞挂起等待,需要我们用异常的形式通知来清楚这个中断标志,当出现这个异常以后,线程不会直接中断而是要看catch中的代码,可以忽略这个异常,也可以直接结束线程。

2)

     否则,只是内部的⼀个中断标志被设置,thread可以通过

    ◦Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在sleep也可以马上收到。

使用join()方法等待

简单来说join方法我们通常用来使一个线程等待另一个线程执行完毕

同时join方法中会有不同的参数如下图:

      第一个无参join方法我个人不推荐使用,因为如果另一个线程一直不结束,这个线程就会一直等待,我们也称为死等

      第二个带有一个参数哥join方法是带有超时时间的等,一旦等待线程等了超过这个时间,就不等了,仍然执行自己之后的代码。默认的单位是毫秒。第三个方法则是第二个join方法的细分,可以支持更高的精度。

  获取当前线程的引用

public class text {
    public static void main(String[] args) {
        Thread t1=Thread.currentThread();
        System.out.println(t1.getId());
      }
}
使用sleep()休眠当前线程

    sleep是Thread中给我们提供的方法,而且sleep会抛出一个异常,如果我们想要中断线程,可以在抛出异常并且catch后进行处理

    很多人会把sleep会wait弄混,等之后讲到线程安全时,我们会着重来讲一下他们两个的区别。

(四)线程在执行时会出现的状态

首先我们要明确,线程的状态是枚举类型的Thread.State

线程的六大状态

• NEW:安排了⼯作,还未开始⾏动

• RUNNABLE:可⼯作的.⼜可以分成正在⼯作中和即将开始⼯作.

• BLOCKED:这⼏个都表⽰排队等着其他事情

• WAITING:这⼏个都表⽰排队等着其他事情

• TIMED_WAITING:这⼏个都表⽰排队等着其他事情

• TERMINATED:⼯作完成了.

如果用代码来表示:

NEW状态是,我们创建出了一个线程,但是我们并没有调用start方法启动

RUNNABLE状态是,我们调用了线程的start方法

BLOCKED状态是线程进入阻塞,这个涉及要加锁,后面细说。

WAITING状态时线程等待,也就是调用了上述的join方法和wait方法注意是调用的无参数,也就是死等的方法。

TIMED_WAITING状态也是线程等待,但是是有超时时间的等.

TERMINATED状态时线程执行完的状态

线程状态之间是如何变化的

我们用一张图来解释

线程状态总结:

• BLOCKED表⽰等待获取锁,WAITING和TIMED_WAITING表⽰等待其他线程发来通知.

• TIMED_WAITING表示线程在等待唤醒,但设置了时限;WAITING线程在⽆限等待唤醒

   我们可以使用jconsole来查看当前线程的状态,但是有时线程的状态变化很快,我们可以适当的加一些方法让线程进行等待。

(五)多线程带来的风险-----线程不安全

观察线程不安全是什么样子
public static int count=0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                count++;
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
      }

这里如果正常计算应该是1w,但明显结果跟我们预期的不对,这就是发生了线程安全问题。

为什么会出现线程不安全

  最根本原因是,线程的调度是随机的,在cpu上是抢占式执行的

  其他原因包括:多个线程修改一个变量;线程的操作不是原子的

原子性

  这里我们要说一下操作不是原子是什么意思,操作原子并不代表一条指令,上述代码的count++是一条指令,但仍然会发生线程不安全,所以并非是看上去是原子的就行count++实际上是分为三步的:

1.先从内存中拿到count

2.count+1

3.将新count值写回到内存

   那如果我们不保证原子性会发生什么呢?

   还是以count++为例子,如果有两个线程同时执行count++。第一个线程count已经+1了但是没有写回到内存中。但是第二个线程刚刚从内存中拿到count值,这时候就会发生线程安全问题

   这点和线程随即调度,抢占式执行关系很大,如果线程不是抢占式执行,那么就不会发生这种问题。

可见性

可见性是指,一个线程对共享数据的修改,能立刻被其他线程所看到

JMM中规定了

• 线程之间的共享变量存在主内存(MainMemory).

• 每⼀个线程都有⾃⼰的‘⼯作内存"(WorkingMemory) 

• 当线程要读取⼀个共享变量的时候,会先把变量从主内存拷⻉到⼯作内存,再从⼯作内存读取数据.

• 当线程要修改⼀个共享变量的时候,也会先修改⼯作内存中的副本,再同步回主内存. 由于每个线程有⾃⼰的⼯作内存,这些⼯作内存中的内容相当于同⼀个共享变量的"副本".此时修改线程1的⼯作内存中的值,线程2的⼯作内存不⼀定会及时变化.

上述我们把实际的内存叫做“主内存”。把cpu缓存,寄存器叫做工作内存方便理解

这里解释一下为什么要有工作内存,为什么不直接从主存中取:

    因为cpu缓存和寄存器对数据的处理比较快,而之所以有了缓存和寄存器还需要主存是因为,如果想把缓存和寄存器做的存储量和主存一样大,需要很多“米”。

指令重排序

  这里我们可以用单例模式(懒汉方式实现的代码举例)简单说一下,下一篇会讲到多线程实现单例模式,在用懒汉方式下实现的单例模式,我们会在一个方法里真正的创建这个对象,但是在new的时候可能会发生指令重排序,所以我们需要使用volital关键字来解决。

如何解决线程不安全问题(简略)

对上述代码进行改进(使用synchronized加锁)

   public static void main(String[] args) throws InterruptedException {
        Object lock=new Object();
         Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
      }

这样我们的结果就正确了,而synchronized方法我们会在下一篇博客详细讲解

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值