JavaSE基础之线程

      在介绍线程之前,先理解一下"程序"的概念:

      程序:即为一条条的按照语法规则排列起来的指令序列。

      程序默认的是保存在外存(硬盘)中,程序在运行的时候,被操作系统装载到内存中,然后CPU从内存中读取并执行。当一个程序被装载到内存,并且被CPU执行的时候,这是这个程序就变成了一个正在执行的程序,也就是所谓的进程

       那我们感到有一点奇怪,我的计算机只是8核的,但是进程数远远多于8个,那操作系统是如何做到多进程同时进行的呢?原来是将时间进行切分,在不同的时间片断内执行不同的进程,这样多个进程就可以交替执行,看上去仿佛是同时进行,如下:

       如上,A和B两个进程是交替执行的,观察A的执行情况是:运行一段时间,再停留一段时间,在继续运行一段时间,如此往复运行。

        那么,什么又是线程呢?即为在一个进程中同时执行的多个操作,称为线程;

       那进程和线程有什么区别呢?

      1.进程是可以单独执行的,而线程是不能单独执行的;

      2.操作系统会为每个进程分配内存空间,但是不会为线程分配空间;

      3.线程必须运行在进程之内,换言之:没有进程,就不会有其内线程;

      4.线程实际上是进程中的一段代码,只不过这段代码可以独立的进行;

       那操作系统又是如何做到多线程同时进行的呢?和多进程同时进行同样的道理:将所在的进程时间单位切片,在不同的时间片段内执行不同的线程

      以下是Java程序运行的简略图:

       由上图可以看出,我们写的或运行的程序都是一个个线程,都是需要强烈依赖JVM虚拟机这个进程才能工作,离开虚拟机是不能工作的,所以Java是线程级别的,不像C和C++写出来的都是".exe"文件,是可以直接运行的程序,他们是进程级别的。

       那么如何创建线程呢?第一种方法是:将其声明为Thread的子类,该子类重写run方法,接下来分配并启动该子类的实例。

package com.Jevin.thread.demo3;

public class ThreadTest1 extends Thread {

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println("小黑线程在计数:i="+i);
        }
    }
}

 

package com.Jevin.thread.demo3;

public class ThreadTest2 extends Thread {

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println("小红在计数:i="+i);
        }
    }
}

       

package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1();
        ThreadTest2 t2 = new ThreadTest2();

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();

        /**
         * 以下是错误的方式:如果用引用直接去调用run()方法,
         * 这不是启动线程,而是调用方法,这样线程会失去并发性
         */
        //t1.run();
        //t2.run();

        /**
         * 除了t1和t2两个线程之外,还有虚拟机创建的主线程,用来执行main()方法
         */
        for(int i=0;i<300000;i++){
            System.out.println("====================主线程在运行");
        }
    }
}

 上面的线程名称"小黑线程"和“小红线程”都是写死的,如何写活呢?请看一下事例:

package com.Jevin.thread.demo3;

public class ThreadTest1 extends Thread {

    public ThreadTest1(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(this.getName()+"在计数:i="+i);
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadTest2 extends Thread {

    public ThreadTest2(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println(this.getName()+"在计数:i="+i);
        }
    }
}

 

package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1("小黑线程");
        ThreadTest2 t2 = new ThreadTest2("小红线程");

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();

        /**
         * 以下是错误的方式:如果用引用直接去调用run()方法,
         * 这不是启动线程,而是调用方法,这样线程会失去并发性
         */
        //t1.run();
        //t2.run();

        /**
         * 除了t1和t2两个线程之外,还有虚拟机创建的主线程,用来执行main()方法
         */
        for(int i=0;i<300000;i++){
            System.out.println("====================主线程在运行");
        }
    }
}

 

      创建线程的第二种方式是:实现Runnable接口,并重写run()方法,然后分配该类的实例,在创建Thread时作为一个参数来传递并启动。

package com.Jevin.thread.demo4;

/**
 * 我们把实现Runnable接口的类称为目标对象;
 */
public class Target1 implements Runnable {

    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println("小黑目标线程正在运行:i="+i);
        }
    }
}
package com.Jevin.thread.demo4;

public class Target2 implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println("小红目标线程正在运行:i="+i);
        }
    }
}
package com.Jevin.thread.demo4;

public class TargetMain {

    public static void main(String[] args) {
        //创建目标对象:
        Target1 target1 = new Target1();
        Target2 target2 = new Target2();

        //创建线程对象,并让线程对象执行指定的目标对象:
        Thread t1 = new Thread(target1); //t1线程对象执行target1目标对象
        Thread t2 = new Thread(target2); //t2线程对象执行target2目标对象

        //启动线程对象:
        t1.start();
        t2.start();
    }
}

上述线程名称写活的方式如下:

package com.Jevin.thread.demo4;

/**
 * 我们把实现Runnable接口的类称为目标对象;
 */
public class Target1 implements Runnable {

    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(Thread.currentThread().getName()+"正在运行:i="+i);
        }
    }
}
package com.Jevin.thread.demo4;

public class Target2 implements Runnable{

    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println(Thread.currentThread().getName()+"正在运行:i="+i);
        }
    }
}
package com.Jevin.thread.demo4;

public class TargetMain {

    public static void main(String[] args) {
        //创建目标对象:
        Target1 target1 = new Target1();
        Target2 target2 = new Target2();

        //创建线程对象,并让线程对象执行指定的目标对象:
        Thread t1 = new Thread(target1,"小黑目标线程"); //t1线程对象执行target1目标对象
        Thread t2 = new Thread(target2,"小红目标线程"); //t2线程对象执行target2目标对象

        //启动线程对象:
        t1.start();
        t2.start();
    }
}

下面介绍一下“用户线程”和“守护线程”;

用户线程:只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程;

守护线程:当JVM发现只有守护线程在运行的时候,JVM会主动的关闭守护线程,再关闭JVM;

先用代码演示用户线程:

package com.Jevin.thread.demo5;

/**
 * 用户线程,只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程
 * 换句话说,只要用户线程没有结束,JVM是不会关闭的;
 * 实际开发中,都是用的是用户线程
 */
public class UserThread extends Thread{

    public UserThread(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<10000000;i++){
            System.out.println(this.getName()+"在计数:i="+i);
        }
    }
}
package com.Jevin.thread.demo5;

public class UserThreadMain {
    public static void main(String[] args) {
        UserThread t = new UserThread("用户线程");
        t.start();
    }
}

这里有个小玩意,就是线程启动的start()方法,可以放在构造器中的,如下:

package com.Jevin.thread.demo5;

/**
 * 用户线程,只要线程没有运行结束,JVM是不会主动停止这个线程的,这种线程称为用户线程
 * 换句话说,只要用户线程没有结束,JVM是不会关闭的;
 * 实际开发中,都是用的是用户线程
 */
public class UserThread extends Thread{

    public UserThread(String name){
        super(name);
        start();
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<10000000;i++){
            System.out.println(this.getName()+"在计数:i="+i);
        }
    }
}
package com.Jevin.thread.demo5;

public class UserThreadMain {
    public static void main(String[] args) {
        UserThread t = new UserThread("用户线程");
    }
}

下面介绍一下守护线程:

package com.Jevin.thread.demo5;

/**
 * 当JVM发现只有守护线程在运行的时候,JVM会主动的关闭守护线程,再关闭JVM
 * 换言之,JVM是不会让守护线程一直运行下去的
 * 表现为:在控制台,有时候没有任何东西打印输出,有时候有一个,有时候有多个
 */
public class DaemonThread extends Thread{

    public DaemonThread(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(this.getName()+"在计数:i="+i);
        }
    }
}
package com.Jevin.thread.demo5;

public class DaemonThreadMain {
    public static void main(String[] args) {

        //线程对象创建好之后,默认是用户线程
        DaemonThread t = new DaemonThread("守护线程");

        //将一个线程标记为守护线程:
        //注意:将一个线程标记为守护线程,一定要在线程启动之前,否则,会出现IllegalThreadStateException异常
        t.setDaemon(true);

        t.start();
    }
}

那么什么时候需要使用”线程”呢?即为当多个操作需要同时执行的时候,必须要使用多线程。

之前写的有个切分文件进行拷贝的博客:https://mp.csdn.net/postedit/83502214

这里我们用多线程实现一下:

package com.Jevin.thread.demo6;

import java.io.File;
import java.text.DecimalFormat;

/**
 * 文件拷贝包工头
 */
public class FileCopyContractor {

    private File srcFile; //源文件
    private int  splitCount; //文件切分的份数

    /**
     * 监工需要知道的条件:
     */
    private String                 desFile; //目标文件
    private String                 tempFile; //拷贝过程中的临时文件
    private long                   fileSize; //文件总的大小
    private FileCopyWorkerThread[] arr; //所有保存工人线程数据的数组

    public FileCopyContractor() {}

    public FileCopyContractor(File srcFile, String desPath, int splitCount) {
        super();
        this.srcFile = srcFile;
        this.splitCount = splitCount;
        //组织目标文件:
        String fileName = srcFile.getName();
        this.desFile = desPath + File.separator + fileName;
        this.tempFile = desFile + ".td";
    }

    public FileCopyContractor(String srcFile, String desPath, int splitCount) {
        this(new File(srcFile), desPath, splitCount);
    }

    /**
     * 包工头开始工作:
     */
    public void assignWork() {

        //获取原文件的大小:
        fileSize = srcFile.length();
        System.out.println("文件的大小是:" + fileSize);

        //根据切分的份数和源文件的大小,计算每个工人的平均工作量
        long perWorkerSize = fileSize / this.splitCount;

        /**
         * 创建数组对象
         */
        arr = new FileCopyWorkerThread[this.splitCount];

        //计算第一个工人的开始位置和结束位置:
        long startPost = 0L;
        long endPost = perWorkerSize;

        //包工头创建多个工人:
        for (int i = 0; i < this.splitCount; i++) {
            //创建工人对象:
            FileCopyWorkerThread fileCopyWorkerThread = new FileCopyWorkerThread("工人-" + i, srcFile, tempFile, startPost, endPost);

            /**
             *启动工人线程,工人开始工作:
             */
            fileCopyWorkerThread.start();

            /**
             * 把工人保存到数组中
             */
            arr[i] = fileCopyWorkerThread;

            //包工头计算下一个工人的开始位置和结束位置:
            startPost = endPost;
            endPost = startPost + perWorkerSize;

            //如果是最后一个工人,则做到最后;
            if (i == this.splitCount - 2) {
                endPost = fileSize;
            }
        }

        /**
         * 当所有的工人创建好之后,创建监工线程
         * 这里的CPU使用率将会急速上升,达到80%
         */
        new MonitorThread();
    }

    /**
     * 监工线程的任务:(1)统计总的文件拷贝进度;(2)当拷贝完成之后,将临时文件名称改为目标文件名称
     * <p>
     * 监工线程需要知道的条件: 1.目标文件的名称 2.临时文件的名称 3.文件总的大小 4.所有线程的拷贝总量: (1)获取所有工人线程对象,调取工人线程对象上的getCopyedSize(),加在一起就是拷贝总量
     * (2)包工头将工人信息保存到一个数组中,然后把数组传递给监工
     */
    class MonitorThread extends Thread {

        public MonitorThread(){
            start();
        }

        @Override
        public void run() {

            DecimalFormat df = new DecimalFormat("##.0%");

            //当拷贝没有完成的时候,监工就要一直工作:
            while (!copyIsOver()) {
                long total = totalCopySize(); //取得所有工人的拷贝总量
                if(total == fileSize){
                    break;
                }
                double d = total / (double) fileSize;
                String str = df.format(d);
                System.out.println("拷贝进度"+str);
            }

            try {
                //确保所有的工人线程把流关闭之后,再去给文件改名
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //所有工人完成工作之后,把临时文件名称改成目标文件名称:
            File file1 = new File(tempFile);
            File file2 = new File(desFile);

            if(file1.renameTo(file2)){
                System.out.println(srcFile + "拷贝到"+desFile+"完成,进度100%");
            }else{
                System.out.println("重命名文件失败");
            }
        }

        /**
         * 取得所有工人的拷贝总量
         *
         * @return
         */
        private long totalCopySize() {
            long total = 0L;
            for (FileCopyWorkerThread work : arr) {
                total += work.getCopyedSize();
            }
            return total;
        }

        /**
         * 判断拷贝操作是否已经完成 当所有的工人都已经完成的时候,就说明拷贝完成了 只要有一个工人拷贝未完成,则说明整个拷贝工作未完成
         *
         * @return
         */
        private boolean copyIsOver() {
            for (FileCopyWorkerThread work : arr) {
                if (work.isAlive()) {
                    return false;
                }
            }
            return true;
        }
    }
}
package com.Jevin.thread.demo6;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 文件拷贝工人
 */
public class FileCopyWorkerThread extends Thread {
    private String name; //工人名称
    private File srcFile; //源文件
    private String desFile; //目标文件
    private long startPost; //开始位置
    private long endPost; //结束位置
    private long copyedPost; //已经拷贝的位置

    public FileCopyWorkerThread(String name, File srcFile, String desFile, long startPost, long endPost) {
        super();
        this.name = name;
        this.srcFile = srcFile;
        this.desFile = desFile;
        this.startPost = startPost;
        this.endPost = endPost;
        this.copyedPost = this.startPost; //初始化拷贝位置即为初始位置

        //System.out.println(name+"[开始位置是:"+this.startPost+",结束位置是:"+this.endPost+"]");
    }

    /**
     * 取得工人已经拷贝的数量
     * @return
     */
    public long getCopyedSize(){
        return this.copyedPost - this.startPost;
    }

    /**
     * 工人开始工作
     */
    @Override
    public void run(){
        RandomAccessFile rin = null; //读数据流
        RandomAccessFile rout = null; //写数据流

        try {
            rin = new RandomAccessFile(this.srcFile,"r");
            rout = new RandomAccessFile(this.desFile,"rw");

            //定位读写的位置:
            rin.seek(this.startPost); //开始读的位置
            rout.seek(this.startPost); //开始写的位置

            byte[] b = new byte[1024*1024];
            int i = 0;
            //当已经拷贝的位置小于结束位置,并且未拷贝到文件结尾,就一直循环拷贝下去:
            while((this.copyedPost < this.endPost) && (i=rin.read(b)) != -1){
                if((this.copyedPost + i) > this.endPost){
                    i = (int) (this.endPost - this.copyedPost);
                }
                rout.write(b,0,i);
                this.copyedPost += i;

                //System.out.println(name+"正在工作,已经拷贝的位置是:"+this.copyedPost+",结束位置是:"+this.endPost);
            }
            //System.out.println(name+"结束工作,已经拷贝的位置是:"+this.copyedPost+",结束位置是:"+this.endPost);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                if(rin != null){
                    rin.close();
                }
                if(rout != null){
                    rout.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.Jevin.thread.demo6;

public class MainTest {
    public static void main(String[] args){
        String file = "D:\\tools\\mysql-8.0.12-winx64.zip";
        FileCopyContractor fileCopyContractor = new FileCopyContractor(file,"d:\\",15);
        fileCopyContractor.assignWork();
    }
}

下面介绍一下线程的优先级:

        如上所示, 它表示该线程被线程调度器选中的概率,其值越大,被线程调度器选中的概率越大,那么就会优先执行完毕。如下代码演示:

package com.Jevin.thread.demo3;

public class ThreadTest1 extends Thread {

    public ThreadTest1(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadTest2 extends Thread {

    public ThreadTest2(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1("小黑线程");
        ThreadTest2 t2 = new ThreadTest2("小红线程");

        /**
         * 设置线程的优先级
         * 注意:改变线程的优先级需要在线程启动之前进行,否则会导致异常
         */
        t1.setPriority(Thread.MAX_PRIORITY); //最高优先级
        t2.setPriority(Thread.MIN_PRIORITY); //最低优先级

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();
    }
}

       但是,我们还是别动其值比较好,默认的各占50%就好,有一个极端情况,加入垃圾回收线程被设置为最低的优先级,那么其被选中回收堆中无用的对象的概率大大降低,那么会造成堆内存越来越大,这样会极大的影响程序的运行,从而导致死机。

下面介绍两个相似的方法:yield()和sleep()方法的使用:

 

 yield()方法翻译过来是:当前线程放弃当前该处理器的使用;

两种理解:(1)该线程yield()后放弃执行,让其他的线程执行

                   (2)该线程yield()后放弃执行,回到线程就绪队列,有可能被线程调度器选中,再次执行

下面由代码演示:

package com.Jevin.thread.demo3;

public class ThreadTest1 extends Thread {

    public ThreadTest1(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());

            /**
             * 这里小黑线程每次打印一句,就yield(),即为放弃的意思:
             * 1.放弃执行,让小红线程执行;
             * 2.放弃执行,重新回到线程就绪队列,有可能被线程调度器选中,再次执行
             * 如果是第一种情况,那么小黑线程应该是不连续打印的,但是实际控制台情况是小黑线程连续执行了,
             * 那么他的功能是第二种情况。
             * 这样造成的结果是:虽然小黑线程的优先级远远高于小红线程,但是由于yield()后,
             * 即使被选中,也放弃本次的执行,小红线程的执行速度会迎头赶上小黑线程
             */
            Thread.yield();
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadTest2 extends Thread {

    public ThreadTest2(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1("小黑线程");
        ThreadTest2 t2 = new ThreadTest2("小红线程");

        /**
         * 设置线程的优先级
         * 注意:改变线程的优先级需要在线程启动之前进行,否则会导致异常
         */
        t1.setPriority(Thread.MAX_PRIORITY); //最高优先级
        t2.setPriority(Thread.MIN_PRIORITY); //最低优先级

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();
    }
}

      上述我们在小黑线程体中yield(),如果是第一种情况,那么,小黑线程应该是不连续打印的,但是实际情形是连续的,那么yield()的正确理解是第二种情况。且我们的小黑线程的优先级远远高于小红线程,如果是第二种情况的话,那么就相当于小红线程被线程调度器选中的概率大大增加,会赶上显黑线程的执行情况,如下:

没有yield()的情况:可见小红线程被小黑线程远远甩在后面:

 yield()后的情况:两个线程的运行相差无几,表明小红线程的优先级似乎提高了。

       sleep(long millis)的翻译是:当前线程暂时停止millis秒钟,也就是说当前线程在暂停期间,不处于线程就绪队列,也就不会被线程调度器选中,那么线程调度器只能选择其他的线程去运行,当该线程sleep结束后,重新返回就绪队列,并有可能被线程调度器重新选中。

代码如下:

package com.Jevin.thread.demo3;

public class ThreadTest1 extends Thread {

    public ThreadTest1(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadTest2 extends Thread {

    public ThreadTest2(String name){
        super(name);
    }

    /**
     * 我们把要执行的操作写在run()方法里面,run()方法又被称为线程体;
     */
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.err.println(this.getName()+"在计数:i="+i+",线程优先级="+this.getPriority());

            try {
                //该线程sleep期间,是不会回到就绪队列的,当睡醒之后,才会重新回到就绪队列
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1("小黑线程");
        ThreadTest2 t2 = new ThreadTest2("小红线程");

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();
    }
}

我们注意到:(1)sleep(long millis)和yield()方法都是static方法,也就是说和对象没关系;

                       (2)在哪个线程中调用sleep()或yield()方法,哪个线程就睡觉或暂停;

如下演示:

package com.Jevin.thread.demo3;

public class ThreadMain {

    public static void main(String[] args) {
        //创建线程对象:
        ThreadTest1 t1 = new ThreadTest1("小黑线程");
        ThreadTest2 t2 = new ThreadTest2("小红线程");

        try {
            /**
             * 我们虽然调用的是t1.sleep(),但是和t1对象没有任何关系
             * 我们是在主线程中调用的sleep(),所以这里是主线程睡觉
             */
            t1.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        /**
         * 线程启动正确的方式:
         * 启动线程的时候,要调用线程的start()方法,线程启动之后会自动执行线程体(也就是run()方法)
         */
        t1.start();
        t2.start();
    }
}

        如上所示,虽然我们使用的是t1.sleep(10000),但是实际上睡的不是t1线程,而是主线程,所以代码的实际运行情况是:点击run运行时,10秒钟内控制台没有任何反应,10秒钟后t1和t2线程相继运行。

下面介绍一下join()方法的运用:

jdk中的解释很简单,即为:等待该线程终结; 换言之,只要该线程没有终结,其他线程不会执行。

       下面我们用代码演示:达到一个这个需求:在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组,只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素,如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义;

package com.Jevin.thread.demo7;

/**
 * 初始化数组线程
 */
public class InitArrayThread extends Thread {

    private String[] arr;

    public InitArrayThread(String name, String[] arr) {
        super(name);
        this.arr = arr;
        start();
    }

    @Override
    public void run() {
        for (int i = 0; i < arr.length; i++) {
            String str = "hello-" + i;
            arr[i] = str;
            System.err.println(this.getName() + "正在初始化元素:" + str);
        }
    }
}
package com.Jevin.thread.demo7;

public class MainThread {

    public static void main(String[] args) {
        /**
         * 在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组
         * 只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素
         * 如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义
         */
        String[] arr = new String[50];

        //创建初始化数组线程
        InitArrayThread t1 = new InitArrayThread("初始化线程", arr);

        //在主线程中打印数组元素
        for (int i = 0; i < arr.length; i++) {
            System.out.println("在主线程中打印数组元素,arr[" + i + "]=" + arr[i]);
        }
    }

}

       我们需要的是当初始化线程将数组初始化完成之后,然后主线程中打印这些数组元素,换言之:主线程和初始化线程都应该有元素的,不为空;但实际情况是,主线程中为空;那么,我们如何达到我们的要求呢?这就要用到join()方法。一下代码演示:

package com.Jevin.thread.demo7;

public class MainThread {

    public static void main(String[] args) {
        /**
         * 在主线程中创建一个包含50个元素的数组,然后创建一个初始化数组的线程对象来初始化数组
         * 只有当初始化数组线程将数组元素初始化完成之后,在主线程中打印数组元素
         * 如果数组元素没有初始化完成,主线程中会打印一堆的null,没有意义
         */
        String[] arr = new String[500];

        //创建初始化数组线程
        InitArrayThread t1 = new InitArrayThread("初始化线程", arr);

        //不准确的控制方式
        //try {
        //    /**
        //     * 主线程睡10毫秒,在这10毫秒之内,线程就绪队列中只有初始化数组线程;
        //     * 换言之:在主线程睡的10毫秒内,初始化线程将数组初始化完成
        //     * 但是,我们不知道主线程到底要睡多少时间,初始化线程才能初始化数组完成,随意这个时间是蒙出来的,不准确
        //     */
        //    Thread.sleep(10);
        //} catch (InterruptedException e) {
        //    e.printStackTrace();
        //}

        //准确的控制方式
        try {
            /**
             * 我们在主线程中调用t1线程的join()方法,则主线程会被阻塞,
             * 直到t1线程运行结束,主线程才会解除阻塞,重新回到就绪队列
             * 换言之:只要t1线程没有运行结束,主线程就不会执行
             */
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //在主线程中打印数组元素
        for (int i = 0; i < arr.length; i++) {
            System.out.println("在主线程中打印数组元素,arr[" + i + "]=" + arr[i]);
        }
    }

}

下面介绍一个银行取款使用线程的知识,如下模型:

       我们先分析一下:按照线程的执行原理:在自己的时间片段内交替执行。如果小明线程执行到第2步,1000>600,走第3步,但是这个时候,小明线程的时间片段到了,但遗憾的是,小明还没有取到钱;再走小红片段,也走到第2步,1000>900,走第3步,此时,小红的时间片也到了;再走小明线程,小明取走了600元,再走小红,小红取走了900元;那么,600+900>1000元,银行亏了?

       下面代码演示:

package com.Jevin.thread.demo8;

/**
 * 账户类
 */
public class Account {

    private int balance = 1000; //账户余额

    /**
     * 取款方法
     *
     * @param money 取款金额
     * @return
     */
    public int withdrawal(int money) throws Exception {
        if (this.balance >= money) {
            /**
             * 这里Thread.sleep(10);并不是让账户对象睡觉,因为账户对象不是线程
             * 这里的取款方法是线程对象调用的,所以哪个线程调用该方法,那个线程睡觉
             */
            Thread.sleep(10); //用来模拟当前线程时间片段结束
            this.balance -= money;
            return money;
        } else {
            throw new Exception("账户余额不足");
        }
    }
}
package com.Jevin.thread.demo8;

/**
 * 小明线程
 */
public class XiaomingThread extends Thread {

    private Account account;

    public XiaomingThread(String name,Account account){
        super(name);
        this.account=account;
        start();
    }

    @Override
    public void run() {
        try {
            int money = account.withdrawal(600);
            System.out.println(this.getName()+"取款成功,金额是:"+money);
        } catch (Exception e) {
            System.err.println(this.getName()+"取款遇到异常,异常信息是:"+e.getMessage());
            e.printStackTrace();
        }
    }
}
package com.Jevin.thread.demo8;

/**
 * 小红线程
 */
public class XiaohongThread extends Thread {

    private Account account;

    public XiaohongThread(String name,Account account){
        super(name);
        this.account=account;
        start();
    }

    @Override
    public void run() {
        try {
            int money = account.withdrawal(900);
            System.out.println(this.getName()+"取款成功,金额是:"+money);
        } catch (Exception e) {
            System.err.println(this.getName()+"取款遇到异常,异常信息是:"+e.getMessage());
            e.printStackTrace();
        }
    }
}
package com.Jevin.thread.demo8;

public class ThreadMain {
    public static void main(String[] args) {
        //创建账户对象:
        Account account = new Account();

        //创建线程对象:
        XiaomingThread t1 = new XiaomingThread("小明",account);
        XiaohongThread t2 = new XiaohongThread("小红",account);
    }
}

执行结果为:符合预期结果

 那么该如何解决这个问题呢?我们可以使用synchronized(this){}同步块来解决,如下:

/**
     * 取款方法
     *
     * @param money 取款金额
     * @return
     */
    public int withdrawal(int money) throws Exception {

        /**
         *synchronized (this){}称为"同步块",
         * 当一个线程A获得锁进入同步块之后,其他线程无法进入到这个同步块的,
         * 只有线程A执行完毕,从同步块中退出之后,其他线程才能竞争获得锁进入同步块
         */
        synchronized (this){

            System.out.println(Thread.currentThread().getName()+"进入到同步块");

            if (this.balance >= money) {
                /**
                 * 这里Thread.sleep(10);并不是让账户对象睡觉,因为账户对象不是线程
                 * 这里的取款方法是线程对象调用的,所以哪个线程调用该方法,那个线程睡觉、
                 *
                 * 当一个线程获得锁进入同步块之后,即使执行了sleep(long millis)/yield()也是不会放锁的
                 * 如果执行的是sleep(long millis),线程不会放锁的,而是睡醒之后继续执行
                 * 如果执行的是yield(),则该线程不会放弃CPU,而是继续执行(如果没有锁,则该线程放弃CPU,重新回到就绪队列)
                 */
                Thread.sleep(10000); //用来模拟当前线程时间片段结束
                this.balance -= money;
                return money;
            } else {
                throw new Exception("账户余额不足");
            }
        }
    }

运行结果如下:

 完美解决。

      上述synchronized(this){}  1.锁的是方法的全部代码;2.并且是使用this作为锁的;那么可以将synchronized提到该方法的返回值之前,如下:

 /**
     * 取款方法
     *
     * @param money 取款金额
     * @return
     */
    public synchronized int withdrawal(int money) throws Exception {

        System.out.println(Thread.currentThread().getName()+"进入到同步块");

        if (this.balance >= money) {
            Thread.sleep(10000); //用来模拟当前线程时间片段结束
            this.balance -= money;
            return money;
        } else {
            throw new Exception("账户余额不足");
        }
    }

那么何时需要线程同步呢?即为:当多个线程同时修改相同数据的时候,必须要线程同步,给数据加锁;

但是线程同步不能滥用,因为线程同步是以牺牲执行速度为代价的!

一下一些线程同步的例子,可以看看:

================================================================================================

下面简单的使用多线程实现一个买票功能:

(1)创建一个车票类Ticket,用String数组保存数据,共有100张车票,也就是该数据保存100个数据

         提供初始化车票分方法public void initTicket(String ticNo){}

         提供卖票的方法,public String sellTicket(){}

(2)创建一个初始化车票线程来初始化车票对象,也就是该线程循环100次,初始化车票

(3)创建4个卖票线程对象,每个卖票线程卖30次(票不够卖)

(4)只有初始化车票的线程把车票初始化完成之后,才能开始卖票

package com.Jevin.thread.demo9;

/**
 * (1)创建一个车票类Ticket,用String数组保存数据,共有100张车票,也就是该数据保存100个数据 提供初始化车票分方法public void initTicket(String ticNo){} 提供卖票的方法,public String
 * sellTicket(){}
 */
public class Ticket {

    /**
     * 保存车票的数组
     */
    private String[] arr   = new String[100];
    private int      index = -1;

    /**
     * 初始化车票的方法,也就是将数据保存到数组中,每次初始化一张车票
     *
     * @param ticketNo
     */
    public void initTicket(String ticketNo) throws Exception {
        if (index < arr.length - 1) {
            index++;
            arr[index]=ticketNo;
        }else{
            throw new Exception("车票满了");
        }
    }

    /**
     * 卖票的方法,返回一张卖出的车票,每次卖票一张
     *
     * @return
     */
    public synchronized String sellTicket() throws Exception {
        if(index>=0){
            String ticketNo=arr[index];

            /**
             * 这里会出现“重票”的问题
             */
            //Thread.yield(); //重票概率小
            //Thread.sleep(10); //重票概率大

            arr[index]=null;
            index--;
            return ticketNo;
        }else{
            throw new Exception("车票卖完了");
        }
    }
}
package com.Jevin.thread.demo9;

/**
 *创建一个初始化车票线程来初始化车票对象,也就是该线程循环100次,初始化车票
 */
public class InitTicketThread extends Thread {

    private Ticket ticket;

    public InitTicketThread(String name,Ticket ticket){
        super(name);
        this.ticket=ticket;
        start();
    }

    @Override
    public void run() {
        try {
            for(int i=0;i<200;i++){
                String ticketNo="第"+i+"号车票";
                ticket.initTicket(ticketNo);
                System.err.println(this.getName()+"初始化车票成功,车票是:"+ticketNo);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.Jevin.thread.demo9;

/**
 * 卖票线程,每个卖票线程卖30次
 */
public class SellTicketThread extends Thread {

    private Ticket ticket;

    public SellTicketThread(String name, Ticket ticket) {
        super(name);
        this.ticket = ticket;
        start();
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i <= 30; i++) {
                String ticketNo = ticket.sellTicket();
                System.out.println(this.getName() + "第" + i + "次卖票成功,卖的车票是" + ticketNo);
            }
        } catch (Exception e) {
            System.err.println(this.getName() + "卖票遇到异常,异常信息是:" + e.getMessage());
            e.printStackTrace();
        }
    }
}
package com.Jevin.thread.demo9;

public class TicketMain {
    public static void main(String[] args) {
        //创建车票对象:
        Ticket ticket = new Ticket();

        //创建初始化车票线程对象:
        InitTicketThread initTicketThread = new InitTicketThread("初始化线程对象",ticket);

        //只有初始化车票的线程把车票初始化完成之后,才能开始卖票
        try {
            initTicketThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //创建四个卖票线程:
        SellTicketThread s1=new SellTicketThread("卖票线程1",ticket);
        SellTicketThread s2=new SellTicketThread("卖票线程2",ticket);
        SellTicketThread s3=new SellTicketThread("卖票线程3",ticket);
        SellTicketThread s4=new SellTicketThread("卖票线程4",ticket);
    }
}

       那么,我们考虑另一个问题?多个线程同时访问同一个对象上不同的方法,修改相同的数据,这种情况有可能发生吗?答案是肯定会。这才是正常的情况,例如:订票和退票,存款和取款等。

我们创建一个后进先出的Stack栈类,一个线程专门压栈,一个线程专门弹栈;

package com.Jevin.thread.demo10;

/**
 * 遇到的问题:
 *  1.消费者取出null
 *  2.消费者遇到-1的数组索引越界异常
 *  3.生产者者遇到100的数组索引越界异常
 *
 *  解决以上的问题:
 *  (1)给push()和pop()加synchronized,可以解决消费者取出null的问题
 *  (2)利用wait()和notify()解决消费者和生产者数组越界异常
 */
public class Stack {

    private String[] arr   = new String[100];
    private int      index = -1;
    private boolean dataIsReady = false; //数据是否准备好,false表示没数据

    /**
     * 压栈
     *
     * @param str
     */
    //当生产者访问压栈的方法时,消费者无法访问弹栈的方法:
    public synchronized void push(String str) {

        //当数组中没有数据的时候(dataIsReady = false),生产者应该生产数据,改变标志变量,唤醒消费者线程
        //当数组中有数据的时候(dataIsReady = true),生产者应该被阻塞
        try {
            if(dataIsReady){
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        index++;
        Thread.yield();
        arr[index] = str;
        dataIsReady = true; //此时,数组中已经放入数据,改变标志变量
        this.notifyAll(); //唤醒消费者线程
    }

    /**
     * 弹栈
     *
     * @return
     */
    //当消费者访问弹栈的方法时,生产者无法访问压栈的方法:
    public synchronized String pop() {

        //当数组中没有数据的时候(dataIsReady = false),消费者应该被阻塞
        //当数组中有数据的时候(dataIsReady = true),消费者应该消费数据,改变标志变量,唤醒生产者线程
        try {
            if(!dataIsReady){
                this.wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        String str = arr[index];
        Thread.yield();
        arr[index] = null;
        index--;

        dataIsReady = false; //此时,数组中没有数据,改变标志变量
        this.notifyAll(); //唤醒生产者

        return str;
    }
}
package com.Jevin.thread.demo10;

/**
 * 生产者线程,专门用来压栈
 */
public class ProducerThread extends Thread {

    private Stack stack;

    public ProducerThread(String name, Stack stack) {
        super(name);
        this.stack = stack;
        start();
    }

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            String str = "Hello-" + i;
            stack.push(str);
            System.err.println(this.getName() + "第" + i + "次压栈成功,压栈的数据是:" + str);
            Thread.yield();
        }
    }
}
package com.Jevin.thread.demo10;

/**
 * 消费者线程,专门用来弹栈
 */
public class ConsumerThread extends Thread {

    private Stack stack;

    public ConsumerThread(String name, Stack stack) {
        super(name);
        this.stack = stack;
        start();
    }

    @Override
    public void run() {
        for (int i = 1; i < 200; i++) {
            String str = stack.pop();
            System.out.println(this.getName() + "第" + i + "次弹栈成功,数据是:" + str);
            Thread.yield();
        }
    }
}
package com.Jevin.thread.demo10;

public class ThreadMain {

    public static void main(String[] args) {

        //创建栈对象:
        Stack stack = new Stack();

        //创建生产者线程:
        ProducerThread producerThread = new ProducerThread("生产者线程", stack);

        //创建消费者线程:
        ConsumerThread consumerThread = new ConsumerThread("消费者线程", stack);
    }

}

下面介绍另一个有意思的问题:如何使用同步的集合?例如:一个线程向集合中添加数据,另一个线程从集合中删除数据:

有以下几种思路:

(1)使用一些线程安全的老的集合:如Vector,HashMap等

(2)可以使用Collections中的方法,将线程不安全的集合转变为线程安全的集合,如下:

 线程这块大致到这里,以后有机会再补充!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值