Java基础----多线程详解

一、简介

1、什么是线程?

要解释线程,就必须明白什么是进程。

2、什么是进程呢?

    进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间),比如用户点击桌面的IE浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。

要点:用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

 

二、线程--概念

在明白进程后,就比较容易理解线程的概念。

1、什么是线程呢?

    是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。同一进程中的多个线程之间可以并发执行。

写的太官方了,我的理解就是:

进程就类似一个班级,线程就类似这个班级里面的小学生,多个小学生可以同时做一件事,也可以排队等前面一个小学生做完(synchronized,同步锁操作);

可以让出当前的空间(yield),让另外一个小学生做,如果别人竞争不到或者没有人跟你竞争,那就自己继续干活;

也可以适当的休息一段时间(sleep),让另外一个小学生干活,之后再加入;

如果有两个要好的朋友,一个做完活了,要走了,你想让他等你,你可以用(join)让他等你做完活;

看过一个总结的特别简单的句子(进程就是打开一个APP,线程就是APP里面的各种操作)

2、线程的类型

线程中有两种类型,用户线程和守护线程

用户线程:运行在前台,执行具体的任务,程序的主线程,连接网络的子线程都是用户线程

守护线程:运行在后台,为其他前台线程服务

守护线程的特点:一旦所有的用户线程都结束运行,守护线程随着用户线程的结束而结束

(简单理解:用户线程就好比公司里面的CEO,经理这些,守护线程就好比保安,CEO,经理都下班了,保安也跟着下班呗)

设置线程成为守护线程(Daemon)

Thread thread = new Thread();
thread.setDaemon(true); // 调用对象的setDaemon方法,设置为守护线程,必须在start()前
thread.start();

三、Thread类和Runnable接口

如果想要在main方法中,定义多个线程去执行一段代码,java语言中实现了多线程编程的类和接口,在java.lang包中定义了Thread类和Runnable接口。

Thread类实现了Runnanble接口。Runnable对象为可运行对象,一个线程的执行是执行该对象里面的run方法

Thread的构造方法如下:

public Thread() 
public Thread(Runnable target)
public Thread(String name)
public Thread(Runnable target, String name)
public Thread(ThreadGroup group, Runnable target)
public Thread(ThreadGroup group, String name)
public Thread(ThreadGroup group, Runnable target, String name)

/**
target:为线程的目标对象,即线程调用start()方法启动后运行那个对象的run()方法
name: 为该线程的名称
group:指定该线程属于哪一个组
*/

Thread的常用方法有:

public static Thread currentThread(); // 返回当前正在执行的线程对象
public void getName();// 返回当前的线程名称
public static void sleep(long millis); // 让当前线程休眠一段时间,让出当前CPU
public static void yield(); // 让出当前CPU,如果存在线程等待并且优先级别高的情况,则该线程获取到CPU执行空间;如果存在同等线程级别的情况,有可能当前让出的线程继续抢到CPU执行空间
public void run(); // 线程的线程体,方法都是写在这里面
public void start(); //JVM调用线程的run方法,启动线程开始
public void setDaemon(boolean on); // 设置线程为守护线程

四、线程的创建

创建线程有两种方法:

1、继承Thread并覆盖run方法

2、实现Runnable接口并重写run方法

继承Thread方法

简单做一个输出

public class ThreadA extends Thread {
    private String name;
    public ThreadA(String name){
        super(name);
        this.name = name;
    }
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + " 线程开始执行 ");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 "+name+"运行:"+i);
            try{
                sleep((int)Math.random()*10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.printf(Thread.currentThread().getName()+" 线程运行结束 ");
    }
}

main方法执行

public static void main(String [] args){
         System.out.println(Thread.currentThread().getName()+"主线程开始运行");
        ThreadA threadA = new ThreadA("A");
        ThreadA threadA1 = new ThreadA("B");
        threadA.start();
        threadA1.start();
        System.out.println(Thread.currentThread().getName()+"主线程结束执行");
}
/**
执行结果:
main主线程开始运行
main主线程结束执行
A 线程开始执行 
子线程 A运行:0
B 线程开始执行 
子线程 B运行:0
子线程 A运行:1
子线程 A运行:2
子线程 A运行:3
子线程 A运行:4
A 线程运行结束 
子线程 B运行:1
子线程 B运行:2
子线程 B运行:3
子线程 B运行:4
B 线程运行结束  

*/

这里可以看出,两个线程互相抢占资源,输出的信息,都是乱的;再看main主线程,一开始就结束了运行,可以看出我们的子线程是执行成功的;

如果我们想要主线程等待子线程执行成功之后,再做下一步操作呢?可以用Join() 方法

public static void main(String[] args){
        System.out.println(Thread.currentThread().getName()+"主线程开始运行");
        ThreadA threadA = new ThreadA("A");
        ThreadA threadA1 = new ThreadA("B");
        threadA.start();
        threadA1.start();
        // join的意思:等待指定线程终止,这里可以理解为: main主线程等待子线程的终止
        threadA.join();
        threadA1.join();
        System.out.println(Thread.currentThread().getName()+"主线程结束执行");
}
/**
main主线程开始运行
A 线程开始执行 
子线程 A运行:0
B 线程开始执行 
子线程 B运行:0
子线程 A运行:1
子线程 A运行:2
子线程 A运行:3
子线程 A运行:4
A 线程运行结束 
子线程 B运行:1
子线程 B运行:2
子线程 B运行:3
子线程 B运行:4
B 线程运行结束 
main主线程结束执行
*/

仔细看输出日志,main主线程结束执行排在最后面;

看的这个排序是在不爽,我们想要让A输出完,B再输出的话,怎么处理呢?

用synchronized 同步锁,他的意思是:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  @Override
    public synchronized void run(){
        System.out.println(Thread.currentThread().getName() + " 线程开始执行 ");
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程 "+name+"运行:"+i);
            try{
                sleep((int)Math.random()*10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+" 线程运行结束 ");
    }

/**
执行结果:
main主线程开始运行
B 线程开始执行 
子线程 B运行:0
子线程 B运行:1
子线程 B运行:2
子线程 B运行:3
子线程 B运行:4
B 线程运行结束 
A 线程开始执行 
子线程 A运行:0
子线程 A运行:1
子线程 A运行:2
子线程 A运行:3
子线程 A运行:4
A 线程运行结束 
main主线程结束执行

*/

嗯,很奈斯

使用Synchronized 表示多个线程同时调用一个方法的时候,没有调用wait()或者sleep()方法的话,只能先进入的一个线程执行完毕之后,后一个线程才能进入。

实现Runnable接口

实现两个线程执行同一个方法,打印输出10、9、8...

// 多个线程,简单输出10、9、8...
public class RunnableA implements Runnable {
    private int count = 10;
    @Override
    public void run() {
        while (count > 0){
            System.out.println("当前线程名称是 >>> "+Thread.currentThread().getName()+" ,count为 >>> "+count);
            count--;
        }
    }
}

调用方法

public static void main(String[] args){
        RunnableA ra = new RunnableA();
        Thread tA = new Thread(ra,"A");
        Thread tB = new Thread(ra,"B");
        tA.start();
        tB.start();
}
/**
执行结果:
当前线程名称是 >>> A ,count为 >>> 10
当前线程名称是 >>> B ,count为 >>> 10
当前线程名称是 >>> B ,count为 >>> 8
当前线程名称是 >>> A ,count为 >>> 9
当前线程名称是 >>> B ,count为 >>> 7
当前线程名称是 >>> A ,count为 >>> 6
当前线程名称是 >>> B ,count为 >>> 5
当前线程名称是 >>> A ,count为 >>> 4
当前线程名称是 >>> A ,count为 >>> 2
当前线程名称是 >>> A ,count为 >>> 1
当前线程名称是 >>> B ,count为 >>> 3
*/

可以看出,两个线程同时执行一个方法,得出的结果存在两个10的情况,我们可以加synchornized 同步锁去处理,但是这样,出现的情况就是要么 tA全执行完,要么tB全执行完,两个线程变成了单个线程去使用。

如果我们想要 tA线程输出双数,tB线程输出单数的情况,应该怎么处理呢?

嘿,上代码

// 多个线程,简单输出10、9、8...
public class RunnableA implements Runnable {
    private int count = 10;

    private int i = 0;
    @Override
    public void run() {

        // synchronized (this) 表示的是该代码块同步,this表示的是当前进来的线程。
        synchronized (this) {
            while (count > 0) {
                notify();   // 唤醒当前进来的线程,如果没有在等待状态,也可以唤醒
                System.out.println("当前线程名称是 >>> " + Thread.currentThread().getName() + " ,count为 >>> " + count);
                count--;
                try {
                    //  wait:使当前线程进入等待状态,并且释放当前对象所持有的锁
                    // (简单理解就是 synchronized 锁住的对象,有方法,有变量,简单理解为当前线程释放了对资源的控制)
                    wait();
                    // System.out.println(Thread.currentThread().getName() + ">>>" + Thread.currentThread().isAlive()+" >>> "+(i++));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


调用方法

public static void main(String[] args){
        RunnableA ra = new RunnableA();
        Thread tA = new Thread(ra,"A");
        Thread tB = new Thread(ra,"B");
        tA.start();
        tB.start();
}

/**
执行结果:
当前线程名称是 >>> A ,count为 >>> 10
当前线程名称是 >>> B ,count为 >>> 9
当前线程名称是 >>> A ,count为 >>> 8
当前线程名称是 >>> B ,count为 >>> 7
当前线程名称是 >>> A ,count为 >>> 6
当前线程名称是 >>> B ,count为 >>> 5
当前线程名称是 >>> A ,count为 >>> 4
当前线程名称是 >>> B ,count为 >>> 3
当前线程名称是 >>> A ,count为 >>> 2
当前线程名称是 >>> B ,count为 >>> 1
*/

【注:博主一直看不懂 wait()  后面说的,并且释放该线程所持有的锁,后面捋了一遍,我的简单粗暴的理解就是:好比有一个屋子,里面很多吃的,一群饿鬼在门外面抢着要进去吃东西,你抢先进去了,然后马上把门关上了,等你吃的差不多了,就要把门打开了。不能这么自私吧。栗子不是很恰当,反正意思就是说,一个线程使用wait()进入等待状态的时候,同时也会释放掉对当前锁的控制,让其他线程可以抢占到锁

总结:

实现Runnable接口比继承Thread具有的优势:

1、避免java中单继承的限制

2、适合多个相同程序的代码去实现同一个资源

3、增强代码的健壮性,代码可以被多个线程共享,代码和数据独立

五、synchronized线程同步

synchronized是java中的关键字,是一种同步锁,修饰的对象有几种

1、修饰一个代码块,被修饰的代码块称为同步语句块,作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码的对象

2、修饰一个类,其作用的范围是synchronized的括号{} 括起来的代码,作用的对象是这个类的所有对象

3、修饰一个方法,被修饰的方法称为同步方法,作用的范围是整个方法,作用的对象是调用这个方法的对象

4、修饰一个静态的方法,作用的范围是整个静态方法,作用的对象是这个类的所有对象

上栗子

synchronized修饰代码块

public void run(){
    synchronized(this){
        System.out.println("修饰一个代码块,作用的对象是调用这个代码块的对象");
    }
}

当两个并发线程同时访问这个代码块的时候,同一时刻只能有一个线程得到执行,另外一个阻塞。必须等到当前线程执行完毕之后才能执行该代码块。

我们也可以通过定义特殊变量来充当锁

private byte[] lock = new byte[0];  // 特殊的instance变量
public void method()
{
      synchronized(lock) {
         // todo 同步代码块
      }
}

说明:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

synchronized修饰方法

public synchronized void run(){
    System.out.println("修饰方法范围是整个函数");
}

这样的写法相当于

public synchronized void methodName(){

}
public void run(){
    synchronized(this){
    }
}

synchronized关键字不能继承。 
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。

synchronized修饰静态方法

public synchronized static void methodName(){
    System.out.println("锁定的是这个类的所有对象");
}

synchronized修饰一个类

public static void method() {
    synchronized(ClassName.class) {
         System.out.println("所有对象用的是同一把锁");
    }
}

总结:

只是单纯做一个笔记总结,期间拷贝一些博主的代码,方便自己日后使用记录。

2019第二天的一个感悟就是:不能活在舒适区啊,尤其是我们程序猿,要时刻有危机感。谨记自己!!!

参考网址链接以下

https://blog.csdn.net/luoweifu/article/details/46595285 (编程思想之多线程与多进程)

https://blog.csdn.net/luoweifu/article/details/46613015#t3  (Java中Synchronized的用法)

https://www.cnblogs.com/GarfieldEr007/p/5746362.html (Java多线程学习(吐血超详细总结)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值