您的位置:首页 > 理论基础 > 计算机网络

多线程,网络编程,别来折磨我?等我看完此篇博客,再和你大战三百回合

2020-08-25 17:54 369 查看

什么是多线程

线程与进程

进程 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
线程 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行,而一个进程最少有一个线程。线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。

举例说明:
假如我们现在正在玩一款游戏,我们在控制我们的英雄去打怪升级,在打的时候,我们的英雄是不是有一连串的动作,还有小怪喊叫的声音,甚至还在播放着背景音乐,小怪的生命值正在下降,我们的能量正在增加,我们的技能正在冷却,那么这一连串的操作,是不是都是在同时进行,而这些操作就是线程。我说同时仿佛不适合,但是我们觉得是在同时进行,其实不是,因为我们的CPU在运行程序时,是采用分时调度,只不过由于CPU执行速度特别快,使得所有程序好像是在同时进行一样。

线程调度

1.分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。

2.抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。

CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

**同步:**排队执行 , 效率低但是安全.
**异步:**同时执行 , 效率高但是数据不安全.

怎么样,线程这东西还是很重要吧,但是要运用好它,可没那么容易哦,现在赶快跟进我的脚步往下看吧

如何实现Java多线程

废话不多说,我们先直接看代码:

在这里插入代码片
public class Demo1 {
public static void main(String[] args) {
Thread t = new MyThread("AAA");
Thread t1 = new MyThread("BBB");
Thread t2 = new MyThread("CCC");
t.start();
t1.start();
t2.start();
}
}

class MyThread extends Thread{
private String title;
public MyThread(String title){
this.title = title;
}
@Override
public void run() {
for(int x = 0; x<10; x++){
System.out.println(this.title+"运行,x="+x);
}
}
}

我的这些代码就是实现多线程。看仔细了,不懂了,再看我后面得解释吧,我们先一步步攻克难关。

我在这里定义了一个线程类MyThread,然后重写了Thread类里的run方法,实现了一个循环输出,因为我们的线程的执行方法通常都是在run方法里执行。
但在执行过程中请注意:不能直接调用此方法,由于线程是并发执行,需要通过操作系统的资源调度才可以执行。
对于多线程的启动我们需要利用Thread类中的start()方法完成,调用它也就间接完成了调用run方法。

谨记: 调用线程是用start(),不是run();

Thread和Runnable

Thread是类,Runnable是接口,这是我们的线程通常需要继承或实现的。不过我们通常都应该选择使用实现Runnable接口,我们现在来简单了解一下Runnable与Thread相比有什么优势。

实现Runnable与继承Thread相比有如下优势

  1. 通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
  2. 可以避免单继承所带来的局限性
  3. 任务与线程是分离的,提高了程序的健壮性
  4. 后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程

现在我们看看如何通过实现Runnable接口来实现多线程,看如下代码:

在这里插入代码片
public class Demo2 {
public static void main(String[] args) {
MyThread1 mt = new MyThread1("AAA");
MyThread1 mt1 = new MyThread1("BBB");
new Thread(mt).start();
new Thread(mt1).start();
}
}

class MyThread1 implements Runnable{
private String title;
public MyThread1(String title){
this.title = title;
}

@Override
public void run() {
for(int x = 0; x<10; x++){
System.out.println(this.title+"运行,x="+x);
}
}
}

Lambda表达式定义线程方法体以及获取线程名称,设置线程名称

照旧,先看代码:

在这里插入代码片
public class Demo3 {
public static void main(String[] args) {
for (int x = 0; x<3; x++){
new Thread(()->{
for(int y = 0; y<10; y++){
System.out.println(Thread.currentThread().getName()+"运行,y="+y);
}
}).start();
}
}
}

怎么样,知道大概意思吧,看到start()了吗?它前面的代码块其实就是代表线程的run()方法里的操作代码块啊,而这就Lambda表达式定义线程方法体。不错吧,通过Lambda表达式替代MyThread子类,是不是使得代码更加简洁。

请仔细看代码,我在我这里涉及到了如何获取线程的名称
格式如下:
Thread.currentThread().getName();
那么如何设置名称呢?
有两种设置名称的方式:
Thread对象.setName(“名称”);
new Thread(new Runnable(),“名称”).start();

Callable接口

实现线程,Runnable接口和Thread类就够了,为什么还有使用Callable接口呢?不知道你们有没有发现?Runnable接口和Thread类里面的run方法不能返回操作结果,那么为了解决这个问题,我们就需要用到Callable接口。

Callable接口定义的时候可以设置一个泛型,此泛型的类型就是call()方法里的返回数据类型,这样也可以避免向下转型所带来的隐患。
因为Callable也是接口,它也只是完成了一任务,它还是需要联系Thread类的,通过Thread类来启动,这个联系是FutureTask来实现的,也可以利用它来获取Callable接口中call()方法的返回值。我们来看代码吧。

在这里插入代码片
public class Demo4 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> task = new FutureTask<>(new MyThread2());
new Thread(task).start();
System.out.println("线程返回数据:"+task.get());
}
}

class MyThread2 implements Callable<String> {

@Override
public String call() throws Exception {
for(int x = 0; x<10; x++){
System.out.println("线程执行:x="+x);
}
return "搞定";
}
}

线程的基本状态

  1. 创建状态 new Thread();表示分配了相应的内存空间和资源。

  2. 等待状态 这个时候已经执行了start()方法启动线程了,只是还需要等待CPU来调度。

  3. 运行状态 这个时候CPU开始来调度了,正在运行run()方法了。

  4. 阻塞状态 有时候为了让程序运行慢一点,或者有秩序一点,我们通常采用一些操作暂时中止某个线程的运行,比如我们
    通常调用sleep(毫秒数),wait();方法就是让线程进入阻塞状态

  5. 死亡状态 当run()方法运行结束后,线程走完了,也就没了。

线程中断

一般Thread类提供的操作方法中很多都会InterruptedException中断异常,在这个时候线程在执行过程中可以被另外一个线程中断执行。如果我们需要中断执行的话,我们可以给我们的线程添加中断标记;
格式为:
Thread对象.interrupt();
同时处理这个异常,在异常下直接return退出方法。线程状态也就中断了。

代码如下,以供参考:

在这里插入代码片
public class Demo5 {
public static void main(String[] args) {
//线程中断
//y一个线程是一个独立的执行路径,它是否结束应该由其自身决定
Thread t1 = new Thread(new MyRunnable());
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("我真的还想再活五百年");
return;
}
}
//给线程t1添加中断标记
t1.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();
System.out.println("发现了中断标记,线程自杀");
return;
}
}
}
}
}

线程同步

虽然使用多线程同时处理资源效率要比单线程高许多,但是多个线程如果操作同一个资源时一定存在一些问题。我们的资源很有可能造成丢失或错误的情况。

在这里插入代码片
public class Demo6 {
public static void main(String[] args) {
//线程不安全
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}

static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {
while (count>0){
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println("卖票结束,余票:"+count);
}
}
}
}

运行结果如下:

看到-1了没?这就是线程同时操作很可能导致的问题。

造成这种情况的原因就是没有将这些线程逻辑单元进行整体性的锁定,在进行判断数据和修改数据时,我们需要只允许一个线程进行处理,而其他线程则只需要等待前面线程执行完毕后再继续执行,这样就可以使得在同一个时间段内,只允许一个线程执行操作,从而实现同步的处理。

实现同步处理有两种方法:

同步代码块

同步代码块是指使用synchronized关键字定义的代码块,在该代码执行时往往需要设置一个同步对象,由于线程操作的不稳定状态,
我们的同步对象最好是选择Object对象,因为它包括了所有。

代码如下:

在这里插入代码片
public class Demo7 {
public static void main(String[] args) {
Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}

static class Ticket implements Runnable{
//总票数
private int count = 10;
private Object o = new Object();
@Override
public void run() {
//Object o = new Object();    //这里不是同一把锁,所以锁不住
while (true) {
synchronized (o) {
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
}else {
break;
}

}
}
}
}
}

同步方法

我们知道同步代码块是在一个方法里的代码块,使得方法的部分操作进行同步处理,但是如果我们需要方法中的全部操作都进行同步处理,就可以采用同步方法的形式进行定义,在方法上一定要使用synchronized关键字进行声明。

代码如下:

在这里插入代码片
public class Demo8 {
public static void main(String[] args) {
//线程不安全
//解决方案2  同步方法
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}

static class Ticket implements Runnable{
//总票数
private int count = 10;
@Override
public void run() {

while (true) {
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if (count > 0) {
//卖票
System.out.println("正在准备卖票");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
return true;
}
return false;

}
}
}

现在我们来看看运行结果:

怎么样,解决了 吧。

线程死锁

死锁,就是指两个线程都是在等对方先执行,就好比警察和罪犯,罪犯挟制人质要求警察放过自己,警察为了要救人质要求罪犯先放人质。于是谁也不肯先执行,就处于一种对峙的状态。

我们来看看代码:

在这里插入代码片
public class Demo9 {
public static void main(String[] args) {
//线程死锁
Culprit c = new Culprit();
Police p = new Police();
new MyThread(c,p).start();
c.say(p);
}

static class MyThread extends Thread{
private Culprit c;
private Police p;
MyThread(Culprit c,Police p){
this.c = c;
this.p = p;
}

@Override
public void run() {
p.say(c);
}
}
static class Culprit{
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放了人质");
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放了,罪犯也放了人质");
}
}
static class Police{
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放了你");
c.fun();
}
public synchronized void fun(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}

虽然我在这里使用同步方法的处理操作,可死锁依然出现,它为什么会出现呢?主要是因为罪犯和警察都在调用对方的say()方法,然后都在等待对方回复,否则自身就执行不下去。
然后就这么卡住了。我们以后尽量避免这种死锁的情况。

多线程经典案例

给大家分享的这个案例叫生产者与消费者。
比如我设置了厨师和服务员两个对象,厨师在做饭代表生产者,服务员在端走饭菜代表消费者。

代码如下:

在这里插入代码片
public class Demo10 {

/**
* 多线程通信问题, 生产者与消费者问题
* @param args
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}

//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}

@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndSaste("老干妈小米粥","香辣味");
}else{
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;

public void setNameAndSaste(String name,String taste){

this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;

}
public  void get(){

System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);

}
}
}

结果如下:

这个程序实现了一个基础的线程交互模型,但是通过执行结果可以发现程序中出现了两个问题:
香辣味和甜辣味错位了
有些饭菜重复操作做了几遍,而我们想的是交替操作。

那么如何解决呢?
厨师在做饭时时,可以让服务器睡会儿,等饭菜做完后,把服务员唤醒,然后厨师睡会儿,服务员送完饭菜后,把厨师唤醒,然后服务员又睡会儿,
然后厨师又开始做饭,就这么一直按顺序走下去,就不会出现数据错位和重复操作了。
所以我们需要用到同步方法或者同步代码块,表示厨师和服务员不能同时都在做事,然后睡一会儿可以用wait()方法表示,唤醒对方用notifyAll()方法,同时需要一个顺序,谁先来,谁后面开始。所以可以定义一个布尔变量,轮流交替执行。

代码如下:

在这里插入代码片
public class Demo10 {

/**
* 多线程通信问题, 生产者与消费者问题
* @param args
*/
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}

//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}

@Override
public void run() {
for(int i=0;i<100;i++){
if(i%2==0){
f.setNameAndSaste("老干妈小米粥","香辣味");
}else{
f.setNameAndSaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0;i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;

//true 表示可以生产
private boolean flag = true;

public synchronized void setNameAndSaste(String name,String taste){
if(flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag) {
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

运行结果:

怎么样,现在没问题吧,没有错位,也没有重复操作,对吧。那我们现在开始进入下一关。

线程池

线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。那么线程池就可为我们解决这个问题。
线程池的主要作用:

  1. 降低资源消耗。
  2. 提高响应速度。
  3. 提高线程的可管理性。

线程池总共分为四个。如下:

缓存线程池

它的线程数没有限制
执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在,则创建线程 并放入线程池, 然后使用

代码如下:

在这里插入代码片
public class Demo11 {
public static void main(String[] args) {
//创建缓存线程池
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()+"对酒当歌,人生几何?");
}
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"对酒当歌,人生几何?");
}
});
}
}

定长线程池

它的线程数是需要指定的,指定多少是多少

执行流程:

  1. 判断线程池是否存在空闲线程
  2. 存在则使用
  3. 不存在空闲线程,且线程池未满的情况下,则创建线程 并放入线程池, 然后使用
  4. 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程

代码如下:

在这里插入代码片
public class Demo12 {
public static void main(String[] args) {
//创建定长线程池,指定两个线程
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
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()+"你好!");
}
});
}
}

单线程线程池

效果与定长线程池 创建时传入数值1 效果一致。

执行流程:

  1. 判断线程池 的那个线程 是否空闲
  2. 空闲则使用
  3. 不空闲,则等待 池中的单个线程空闲后 使用

代码如下:

在这里插入代码片
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());
}
});
}
}

周期性定长线程池

定时执行, 当某个时机触发时, 自动执行某任务 。线程数也是指定的。
除此,它还有4个参数。

  • 参数1. runnable类型的任务
  • 参数2. 时长数字
  • 参数3. 周期时长(每次执行的间隔时间)
  • 参数4. 时长数字的单位

代码如下:

在这里插入代码片
public class Demo14 {
public static void main(String[] args) {
//创建周期性定长线程池
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
//执行一次
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("在吗?");
}
},4, TimeUnit.SECONDS);  //表示5秒后执行

//周期性执行
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("我在!");
}
},5,2,TimeUnit.SECONDS);//表示5秒后执行一次,然后每隔两秒持续执行

}
}

OK,我们的线程就学习到这儿,下面我们了解了解网络编程吧。

什么是网络编程

计算机网络

准确来说就是分布在不同地域内的计算机设备通过一些网络设备和通信链路连接在一起的一个网络系统。主要作用是资源共享,数据通信。

IP地址

IP地址表示我们的计算机在互联网上的唯一标识,我们可以把它看作人的身份证号码。
IP地址:
内网IP,其实就是在一个局域网内的IP。
公网IP,它通常表示的我们这个局域网内拉的一根网线,而这个网线设备的IP地址就是公网IP。
本机IP:表示我们本机的IP地址。地址是127.0.0.1。

域名

我们把IP地址理解为人的身份证号码,那我们可以把域名理解为人的姓名,为什么要有域名呢?因为身份证号码不好记,所以就诞生了域名,特别是从2011年起,由于IP地址不够用,最后变成128位的IPV6版本的IP地址,就更不好记了。
我们通常在浏览器输入的网址,其实就包含了域名,在我们进行放问之后,计算机会先访问域名解析器,然后找到IP地址再进行访问。

端口号

它用来区分在一台计算机下不同的应用程序,端口号的范围是在0—65535之间,在使用时,避免在0–1024之间,因为它们已经被知名软件占用了。

通信协议

它表示的是计算机与计算机之间的交流标准。因为每个计算机设备都不一样,有些设备可能就几百块钱对吧,那有些设备可能十多万,配置有好有差的。它们在传输速率上、传入接口上、步骤控制、出错控制上都各不一样,所以为了能够正常通信,是不是应该制定一个协议,按照一个标准去通信呢。

网络编程程序的分类

B/S程序:浏览器与服务器的程序。比如:百度、4399
C/S程序:客户端与服务器的程序 比如:QQ、微信

现在我们就来学习如何创建C/S程序。

创建C/S程序

创建这个程序,我们需要使用到两个类。
1.ServerSocket 类:用来搭建服务器
2.Socket类 : 用来搭建客户端。
两方都使用Socket进行交流,Socket就是套接字。

搭建服务器

ServerSocket 对象名1 = new ServerSocket(端口号);

我们在搭建的时候需要进行绑定端口号,因为它相当于是这个服务器的标识,客户端需要通过这个端口号来连接它。

等待客户端连接

对象名1.accept();

连接服务器

客户端若想要连接服务器,必须使用Socket类,格式为:

Socket 对象名2  =  new Socket(IP地址,端口号);

客户端与服务器通信

使用IO技术

向通信的另一端发送信息

OutputStream os = (服务器或客户端)对象名.getOutputStream();
PrintStream pw = new PrintStream(os);
pw.println("信息");

接收通信的另一端发送的信息

InputStream is = (服务器或客户端)对象名.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
br.readLine();

注意:
在网络编程时,无论是发送还是接收,对客户端与服务器都是相对的,所以客户端与服务器发送与接收的顺序必须是相反的。
比如客户端先发送,那么服务器必须先接收。
我们为了避免这种麻烦也可以运用线程,一个线程专门用来接收,另一个专门用来发送。

实现多用户访问服务器

可能我们会有疑问,难道只有一个客户端访问服务器吗?那要是有成百上千个客户访问呢?怎么去实现呢?
那我们就不可能等待一个客户端连接吧。我们需要运用循环等待客户端连接,对吧。
用while(true)循环吧,但也有一个问题,岂不是要排队?假如我们这些用户同时访问呢?那我们就在这个循环里使用线程呀。对不对?
但一定请记得,服务器和客户端的发送和接收一定要交替着来。

结语

多敲代码多看书。一起加油。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: