java多线程的那些事儿

一、重要的概念

1.首先我们要了解线程进程的概念,那么什么是进程和线程呢?
进程是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
2.什么是线程调度
第一种是分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
第二种是抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
当然Java使用的是抢占式调度。CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
3.同步与异步
同步:排队执行 , 效率低但是安全.
异步:同时执行 , 效率高但是数据不安全
往往,让1000个线程排队运行的时间效率要高于1000线程同时运行的时间效率。因为,每一线程运行的时间的总和是一样的,但是,同时运行因为有抢占CPU的时间开销从而会有更大的时间开销。
4.并发与并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。(有点让人联想到相对论中同地同时的绝对性,异地同时的相对性(手动狗头))

二、实现多线程的方式

2.1继承Thread类

话不多说,先上代码,结合代码进行分析

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo01 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"线程1").start();
        new Thread(myThread,"线程2").start();
        new Thread(myThread,"线程3").start();
    }
    static class MyThread extends Thread{
        @Override
        public void run() {
            for (int i = 0;i < 5;i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }
}

为了方便观察,所以写了一个静态内部类,静态内部类MyThread继承了Thread类,所以必须要重写Thread的run方法,在方法里,我们可以调用Thread.currentThread().getName()这个方法来显示当前线程的名字。在main方法里我们声明了一个MyThread类的对象,然后直接调用Thread的构造方法,传入对象参数和线程的名字,再调用线程的start()方法,启动线程。来看结果:
在这里插入图片描述

2.2 实现Runnable接口

Runnable接口的用法,其实也相差不大,只是由于java只支持单继承,所以只能继承一个Thread,但是java支持多实现,所以除了Runnable接口以外还可以增加其他接口,线程池的操作也只接收实现Runnable接口的对象,而不接受继承Thread类的对象。

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo02 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        new Thread(myRunnable,"MyRunnable1").start();
        new Thread(myRunnable,"MyRunnable2").start();
        new Thread(myRunnable,"MyRunnable3").start();
    }
    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 0;i < 5;i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }
}

运行结果如下:
在这里插入图片描述

2.3 Callable接口

来看代码示例:

package test01;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/**
 * @author Mr Bai`s Soda
 */
public class Demo03 {
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        new Thread(futureTask,"MyCallable1").start();
        for (int i = 0;i < 5;i++) {
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }

    static class MyCallable implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            for (int i = 0;i < 5;i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
            return null;
        }
    }
}

运行结果如下:
在这里插入图片描述与Runnable有一些不同之处:Callable可以返回执行结果,Callable接口的call()允许抛出异常;Runnable的run()不能抛出。Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

三、线程的操作

3.1 线程休眠

调用sleep()方法,进行线程休眠,能让线程运行不那么快。

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo04 {
    public static void main(String[] args) {
        for (int i = 0;i < 5;i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }
}

sleep()方法括号里传入的是毫秒,这里1000毫秒,就是让线程隔1秒输出一个数字。
在这里插入图片描述

3.2 线程中断

一个线程是一个独立的执行路径,所以一个线程是否结束应当由其自身决定。如果强行进行掐断,很可能导致其正在操作的资源无法被释放。

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo05 {
    public static void main(String[] args) {
        /**创建一个新线程*/
        Thread thread = new Thread(new MyRunnable(),"分线程");
        thread.start();
        /**主线程*/
        for (int i = 0;i < 5;i++) {
            System.out.println(Thread.currentThread().getName()+"->"+i);
            /**休眠1s让其更加明显*/
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        /**给新线程添加中断标记*/
        thread.interrupt();
    }

    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 0;i < 10;i++) {
                System.out.println(Thread.currentThread().getName()+"->"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    /**发现中断标记,可以选择抛出异常*/
                    //e.printStackTrace();
                    /**也可以发现中断标记选择直接return*/
                    return;
                }
            }
        }
    }
}

结果如下:
在这里插入图片描述

3.3 守护线程

线程分为用户线程和守护线程,当一个进程不包含存活的用户线程时,进程结束,当最后一个用户线程结束,守护线程结束。
用setDaemon(true)方法标识守护线程。

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo06 {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable,"MyRunnable");
        /**设置为守护线程*/
        thread.setDaemon(true);
        thread.start();
        for (int i = 0;i < 5;i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"->"+i);
        }
    }
    static class MyRunnable implements Runnable {

        @Override
        public void run() {
            for (int i = 0;i < 10;i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"->"+i);
            }
        }
    }
}

看结果:
在这里插入图片描述
很明显,因为thread线程是守护线程,当用户线程执行结束以后,守护线程也不再执行。(这个地方有的人看完这段代码有可能会钻一个牛角尖,但是你仔细想一下,你的那个想法是错误的)

四、线程安全

记得在学校上java课的时候,那个时候老师留了一道多线程的题目。(南邮物联网专业,如果有学弟学妹搜到这篇帖子,希望能给你们带来帮助)反正那当时确实是不会做,老师讲了synchronized ()的方法,因为没听明白,所以还是不会用,其实非常简单的一个道理,那道题是这样的:
Java多线程编程练习:
(1) HomeWork类继承Thread
(2) HomeWork类有一静态变量tick,其值为20
(3) HomeWork类中实现run方法,在run方法中,在while循环中判断tick的值。如果tick的值大于0,对tick的值减1,并打印出tick的值。然后,sleep 1 秒钟。
(4) 在main函数中,生成3个HomeWork类的线程,并启动线程。
要求最终输入如下,即数字是依序递减。即便多次运行,输出的数字顺序保持不变。
线程1输出数字:20
线程3输出数字:19
线程2输出数字:18
线程3输出数字:17
线程1输出数字:16
线程2输出数字:15
线程1输出数字:14
线程2输出数字:13
线程3输出数字:12
线程2输出数字:11
线程3输出数字:10
线程1输出数字:9
线程2输出数字:8
线程3输出数字:7
线程1输出数字:6
线程2输出数字:5
线程3输出数字:4
线程1输出数字:3
线程2输出数字:2
线程3输出数字:1

我们先看一看线程不安全情况下的结果:
代码:

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo07 {
    public static void main(String[] args) {
        HomeWork homeWork = new HomeWork();
        new Thread(homeWork,"线程1").start();
        new Thread(homeWork,"线程2").start();
        new Thread(homeWork,"线程3").start();
    }

    static class HomeWork extends Thread {
        public static int ticket = 20;
        @Override
        public void run() {
            while (ticket > 0) {
                System.out.println(Thread.currentThread().getName()+"输出数字:"+ticket--);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

结果:
在这里插入图片描述出现了多个线程输出同一个数字的情况,非常的不安全,也没有按照顺序递减进行输出。

接下来,我们来看一看解决这个题目的三个方法。

4.1 同步代码块–synchronized

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo08 {
    public static void main(String[] args) {
        HomeWork homeWork = new HomeWork();
        new Thread(homeWork,"线程1").start();
        new Thread(homeWork,"线程2").start();
        new Thread(homeWork,"线程3").start();
    }

    static class HomeWork extends Thread {
        public static int ticket = 20;
        /**创建一个用于加锁的对象,注意不能放到run方法里面,要不然就是每个线程
         * 都有自己的锁,自己给自己加,然后自己有自己锁的钥匙,那不相当于没有加锁了
         * */
        Object object = new Object();
        @Override
        public void run() {

            while (true) {
                /**把用于加锁的对象传入synchronized方法*/
                synchronized (object)
                {
                    if (ticket > 0) {
                        System.out.println(Thread.currentThread().getName()+"输出数字:"+ticket--);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    } else {
                        break;
                    }
                }
            }
        }
    }
}

看结果:
在这里插入图片描述
明显,达到了题目的要求,为什么会有很多线程接连输出数字呢?因为这个线程“回手掏”的速度很快,刚刚用完资源解开锁,就又一把给锁抢回来了,又给上了锁,其他的线程只能干瞪眼儿,但是也不是抢不到,图中可以看到还是都能够进行数字输出的。

4.2 synchronized同步方法

package test01;

/**
 * @author Mr Bai`s Soda
 */
public class Demo09 {
    public static void main(String[] args) {
        HomeWork homeWork = new HomeWork();
        new Thread(homeWork,"线程1").start();
        new Thread(homeWork,"线程2").start();
        new Thread(homeWork,"线程3").start();
    }
    static class HomeWork extends Thread {
        public static int ticket = 20;

        @Override
        public void run() {
            while (true) {
                /**flag接着fun函数的结果,如果是true接着循环,否则退出循环*/
                boolean flag = fun();
                if (!flag) {
                    break;
                }
            }
        }
        /**synchronized修饰方法,可以返回boolean类型的变量*/
        public synchronized boolean fun() {
            /**ticket大于0,输出成功,返回true,否则返回false*/
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "输出数字:" + ticket--);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return true;
            }
            return false;
        }
    }
}

来看结果:
在这里插入图片描述与题目要求一致。

4.3 显式锁lock

同步代码块和同步方法都属于隐式锁,lock属于显式锁,程序员自己加锁自己解锁。

package test01;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author Mr Bai`s Soda
 */
public class Demo10 {
    public static void main(String[] args) {
        HomeWork homeWork = new HomeWork();
        new Thread(homeWork,"线程1").start();
        new Thread(homeWork,"线程2").start();
        new Thread(homeWork,"线程3").start();
    }

    static class HomeWork extends Thread {
        public static int ticket = 20;
        /**传入的参数为true表示公平锁,false表示不公平锁,前面的同步代码块和同步方法都是不公平锁*/
        public Lock myLock = new ReentrantLock(true);

        @Override
        public void run() {
            while (true) {
                myLock.lock();
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName()+"输出数字:"+ticket--);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
                myLock.unlock();
            }
        }
    }
}

结果:
在这里插入图片描述如果传入的参数是false,那么就不会这个结果就不会这么均匀。

五、线程池 Executors

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。 线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:
1.降低资源消耗。
2.提高响应速度。
3.提高线程的可管理性。

5.1 缓存线程池

package test01;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Mr Bai`s Soda
 */
public class Demo11 {
    public static void main(String[] args) {
        /**缓存线程池没有长度限制
         * 任务加入后的执行流程
         * 1判断线程池是否存在空闲线程  2如果存在则使用   3不存在则创建线程放入线程池并使用
         * 一段时间没有任务以后,线程池会自动关闭
         */
        /**向线程池加入新任务*/
        ExecutorService service = Executors.newCachedThreadPool();
        /**让线程池执行任务*/
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
    }
}

结果如下:
在这里插入图片描述

5.2 定长线程池

package test01;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Mr Bai`s Soda
 */
public class Demo12 {
    public static void main(String[] args) {
        /**缓存线程池没有长度限制
         * 任务加入后的执行流程
         * 1. 判断线程池是否存在空闲线程
         * 2. 存在则使用
         * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
         * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
         */
        /**给线程池指定长度*/
        ExecutorService service = Executors.newFixedThreadPool(3);
        /**让线程池执行任务*/
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
    }
}

结果:
在这里插入图片描述

5.3 单线程线程池

顾名思义,线程池中只有一个线程可供使用,当有多个线程则需要进行排队

package test01;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author Mr Bai`s Soda
 */
public class Demo13 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        });
    }
}

结果:
在这里插入图片描述可以看到全是thread1在执行。

5.4 周期性任务定长线程池

直接上代码:

package test01;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * @author Mr Bai`s Soda
 */
public class Demo14 {
    public static void main(String[] args) {
        /**
         * 周期任务定长线程池.
         * 执行流程:
         * 1. 判断线程池是否存在空闲线程
         * 2. 存在则使用
         * 3. 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池, 然后使用
         * 4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
         *
         * 周期性任务执行时:
         * 定时执行, 当某个时机触发时, 自动执行某任务 .
         */
        ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
        /**
        * 定时执行
        * 参数1. runnable类型的任务
        * 参数2. 时长数字
        * 参数3. 时长数字的单位
        * 在5s后执行
        */
        /**service.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        },5,TimeUnit.SECONDS);
        */
        /**
        * 周期执行
        * 参数1. runnable类型的任务
        * 参数2. 时长数字(延迟执行的时长)
        * 参数3. 周期时长(每次执行的间隔时间)
        * 参数4. 时长数字的单位
        * 在5s后每2s执行一次
        */
        service.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程的名称:"+Thread.currentThread().getName());
            }
        },5,2, TimeUnit.SECONDS);
    }
}

结果:
在这里插入图片描述这是5s后每隔2s执行一次,被注释掉的代码是5s后执行。

六、多线程网络编程实例

(也是实验报告的内容,java期末考试原题,还好当时把代码背下来了)
TCP Socket 多线程通信
(1)服务器端多线程编程,能够接收不同客户端的网络连接;
(2)客户端不需要多线程编程,但可以持续发送消息(不止发送一个消息),消息设定为小写字母的组合;
(3)服务器端接收到用户的消息后,将消息内容由小写字母转变为大写字母,并将转变后的内容发送至客户端,客户端打印服务器返回的消息;
(4)客户端发送“bye”,结束通信。

服务器端:

package test03;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * @author Mr Bai`s Soda
 */
public class Server {
    private static final int SERVER_PORT = 30000;
    public static void main(String[] args) throws IOException{
        ServerSocket serverSocket = new ServerSocket(SERVER_PORT);
        System.out.println("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");
        while(true){
            Socket socket = serverSocket.accept();
            System.out.println("客户端主机地址:"+socket.getRemoteSocketAddress());
            new Thread(new ServerThread(socket)).start();
        }
    }
}
class ServerThread implements Runnable{
    private Socket socket;
    static final String OVER = "bye";

    public ServerThread(Socket socket) throws IOException{
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            while (true){
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String mess = br.readLine();
                System.out.println("客户端发送的信息为:"+mess);
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
                if (mess.equals(OVER)){
                    System.out.println("本次通信结束!");
                    socket.close();
                }else {
                    String mess1 = mess.toUpperCase();
                    System.out.println("响应客户端的信息:"+mess1);
                    bw.write(mess1+"\n");
                    bw.flush();
                }
            }
        } catch (IOException ioException) {
            ioException.printStackTrace();
        }
    }
}

客户端:

package test03;

import java.io.*;
import java.net.Socket;
/**
 * @author Mr Bai`s Soda
 */
public class Client {
    public static void main(String[] args) {

        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1", 30000);
            System.out.println("远程主机地址:" + socket.getRemoteSocketAddress());
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedReader wt = new BufferedReader(new InputStreamReader(System.in));
            while (true) {
                System.out.println("请输入信息:");
                String str = wt.readLine();
                bw.write(str + "\n");
                bw.flush();
                if (str.equals("bye")) {
                    System.out.println("本次通信结束!");
                    break;
                }
                String mess = br.readLine();
                System.out.println("服务器响应:" + mess);
            }
        } catch(IOException ioException){
            ioException.printStackTrace();
        }
    }
}

服务器端运行结果:
在这里插入图片描述客户端运行结果:
在这里插入图片描述

Finally

本文只代表个人收集整理的资料,如有不足请指正!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白的Soda

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值