第四周 线程并发问题部分及网络编程部分

线程并发问题(线程安全)

案例场景

  • 银行取钱问题
  • 买票问题
  • 限时秒杀

线程安全

多线程并发时,多个线程同时操作同一个内存区域(变量),可能会导致的结果不一致的问题;所谓线程安全,指的是在多线程并发操作的时候能保证共享的变量数据一致

并发编程三要素

线程并发时需要保证线程安全,需要满足三大条件:

  • 原子性
  • 可见性
  • 有序性
原子性(Atomicity)

对于一条线程执行来说要保证,要么都成功,要么都失败;对于原子性操作可以通过加锁的方式实现;Synchronized和ReentrantLock保证线程的原子性

可见性(Visibility)

多条线程操作同一个变量时,需要保证其中一条线程对变量的修改,对其他线程也是可见的

有序性(Ordering)

对于每一条线程的执行,内部应该是有序的:

  1. 代码顺序执行
  2. 锁的释放应该在获得之后
  3. 变量读取操作,在修改操作之后

线程同步解决方案

Synchronized

synchronized的关键字使用包含三种方式:

  1. synchronized块:对象锁
  2. synchronized方法:在方法声明的时候使用
  3. 类锁:在静态方法或者同步块中使用类的Class对象

被synchronized鎖住的区域称之为临界区

死锁

​ 由于线程并发,会出现共享变量的情况,如果使用同步锁,对象会被锁住,如果存在多个线程争抢资源时陷入僵局(多个线程在等待被对方线程占有的资源释放时陷入的无限等待状态),这种情况称之为死锁。死锁无法解决,只能尽量避免

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CTrX3RFk-1595765368908)(C:\Users\admin\Desktop\two\20200720\笔记\assets\1595215030827.png)]

public class DeathLock implements Runnable{

    /**
    * 使用静态修饰的原因是希望意向两个对象永远只存在一个实例(不论有多少Runnable对象)
    */
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private int flag;

    public DeathLock(int flag) {
        this.flag = flag;
    }

    @Override
    public void run() {	
        if(flag == 1) {
            synchronized (obj1) {
                System.out.println("线程1锁住了obj1");
                synchronized (obj2) {
                    System.out.println("线程1锁住了obj2,结束。。。。");
                }
            }
        }else if(flag == 2){
            synchronized (obj2) {
                System.out.println("线程2锁住了obj2");
                synchronized (obj1) {
                    System.out.println("线程2锁住了obj1,结束。。。。");
                }
            }
        }
    }

    //HashMap  ConcurrentHashMap
    public static void main(String[] args) {
        DeathLock d1 = new DeathLock(1);
        DeathLock d2 = new DeathLock(2);

        new Thread(d1).start();
        new Thread(d2).start();
    }

}

Lock

​ 从JDK1.5之后新增的并发编程包(java.util.concurrent)中新增了一个新的锁机制:Lock;Lock是一个锁接口,常见的实现类:java.util.concurrent.ReentrantLock(可重入锁);提供了跟synchronized相同的功能,也可以对于的对象实现加锁,ReentrantLock粒度控制方面比synchronized更细,同时支持公平锁和非公平锁(默认),synchronized只支持非公平锁;使用方式:

public class SaleTickets implements Runnable {

	private static int t = 100;
	private static boolean isOver;
    /**创建所对象*/
	private Lock lock = new ReentrantLock(true); 

	@Override
	public void run() {
		while (!isOver) {
       		//获得锁
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "买到票-->" + t--);
                if (t <= 0) {
                    isOver = true;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //释放锁
                lock.unlock();
            }
		}
	}

	public static void main(String[] args) {
		// 创建Runnable对象
		SaleTickets st = new SaleTickets();

		Thread t1 = new Thread(st, "客户A");
		Thread t2 = new Thread(st, "客户B");
		Thread t3 = new Thread(st, "客户C");
		t1.start();
		t2.start();
		t3.start();
	}

}

注意事项:

  1. 锁的获取位置一般在try语句块之前
  2. 锁的释放一般放到finally语句块中

ReentrantLock和Synchronized的区别:

  1. ReentrantLock是一个实现类,后者是一个关键字
  2. ReentracntLock需要手动获取锁以及释放锁(粒度控制更细);后者自动加锁,临界区(synchronized锁住的区域)的代码执行完之后自动释放
  3. ReentrantLock支持公平锁以及非公平锁;sychronized只支持非公平锁
  4. synchronized一般用于少量代码同步,ReentrantLock一般用于大量的同步代码块

volatile

在前面的线程并发中,对于原子性的解决方案使用synchronized或lock实现同步;但是对于数据的可见性来说,我们还需要另外处理,关于可见性,比如:

public class VolatileDemo implements Runnable{

    private int count;
    private boolean isOver;//false

    @Override
    public void run() {
        System.out.println("线程"+Thread.currentThread().getName());
        while(!isOver) {
            count++;
        }
        System.out.println(Thread.currentThread().getName()+"count--->"+count); 
    }

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

        VolatileDemo vd = new VolatileDemo();

        Thread t1 = new Thread(vd,"t1");
        t1.start();
        Thread t2 = new Thread(vd,"t2");
        t2.start();

        Thread.sleep(3000);
        vd.isOver = true;
    }
}

对以上程序的分析,两条子线程启动3秒之后,由于主线程修改过了isOver的变量值,因此预期的结果因该是两条子线程,t1,t2应该会正常结束;但是实际情况是,并没有,效果如下:

线程t1
线程t2

说明主线程对于以上变量的修改,并未立即同步给其他线程;

原因是因为,多线程程序中,jvm为每一条线程单独开辟了一块临时缓存区,该缓存区用于存储主存储器中存储的全局变量的副本;因此在对其中一条线程修改该变量是,该结果并不会立即同步到其他线程,因此会导致在其他线程中不能立即看到该变量的修改(不满足可见性)

所以,如果需要将一条线程修改的变量结果立即同步给其他线程,需要将该变量定义为volatile,因此,对于以上的代码应该修改为:

private volatile boolean isOver;//false

volatile和synchronized的区别:

synchronized保证的是线程执行的原子性,volatile只能保证变量的可见性不能保证原子性

一句话总结volatile关键字的作用:实时的将全局变量的更新立即同步给其他线程

Wait&notify

线程运行时若需要限时等待,则可以通过sleep()方法设置休眠时间,但是对于一些不定时的等待需求sleep则无法实现;对于这种需求,java.lang.Object类中提供了用于实现等待和唤醒的方法:wait和notify;

  • wait()
  • notify()

在使用wait和notify的时候一定要求确保当前线程持有对象的监视器(对象锁)

public class WaitDemo implements Runnable {

    private int count;
    private volatile boolean isOver = false;

    @Override
    public void run() {

        while (!isOver) {
            String tname = Thread.currentThread().getName();
            synchronized (this) {
                try {
                    count++;
                    System.out.println(tname + "---count-->" + count);
                    if (count <= 300000 && (tname.equals("t1") || tname.equals("t2") || tname.equals("t3"))) {
                        // 调用wait以及notify时需要当前线程对象持有目标对象的对象监视器(对象锁)
                        wait();
                    } else {
                        if (count == 1500000) {
                            // 唤醒一条线程(随机)
                            this.notify();
                            // 唤醒在该对象上的所有线程
                            //notifyAll();
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static void main(String[] args) {
        WaitDemo wd = new WaitDemo();

        Thread t1 = new Thread(wd, "t1");
        t1.start();
        Thread t2 = new Thread(wd, "t2");
        t2.start();
        Thread t3 = new Thread(wd, "t3");
        t3.start();
        Thread t4 = new Thread(wd, "t4");
        t4.start();
    }

}

wait()和sleep()区别?

  • sleep是来自Thread类中的一个方法;wait是属于Object类的方法
  • sleep是限时等待,在等待的时限到达时自动恢复;而wait是无限等待,必须要等到其他线程调用该对象上的notify方法(或notifyAll)才能继续执行
  • sleep使用时不需要持有对象锁,而wait使用时必须确保当前线程持有该对象的对象锁,否则会有异常(java.lang.IllegalMonitorStateException)
生产者消费者问题(消息队列)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iRAr8EGm-1595765368918)(C:\Users\admin\Desktop\two\20200720\笔记\assets\1595236615810.png)]

网络编程

计算机网络

概述

​ 将分布在不同地理区域的计算机,通过一些外部网络设备以及内部的网络协议,连接成一个网络系统;通俗的理解为2台或以上的计算机协同工作,例如:文件传输,数据传输。计算机网络的核心目的是实现:信息共享

网络分类

根据网络规模以及用途分为:

  • 局域网(0-10km,通过网络设备有线连接)
  • 城域网(0-100km,交通信号,视频监控)
  • 广域网(因特网:互联网)

根据网络的工作模式分为:

  • 专有服务器(只提供专一某一种服务器,如:云主机,数据库专有服务器,静态资源文件的专有服务)
  • 客户机服务器模式(c/s架构)
  • 对等式网络(peer to peer)

网络模型与协议

计算机网络之间实现通信需要两种设备支持:

  1. 硬件设备:(网卡,网关:交换机,路由器)
  2. 软件设备:(网络通信协议:TCP/IP、UDP)
网络协议

网络协议就是计算器网络设备相互之间通信的标准(暗号),在互联网标准组织的推动下,将网络协议分为两种:

  • TCP/IP : 传输控制协议/ip协议
  • UDP:用户数据报协议
OSI(Open System Interconnection)七层模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hmxKVU10-1595765368920)(D:\带班资料\2020\j2003\线下\part1-JavaSE\20200721\笔记\assets\1595299573815.png)]

TCP/IP协议(打电话)

传输控制协议,是一个安全可靠的互联网协议,需要通信的主机之间需要先建立正确的链接,才能够进行通信,并且改协议能够保证数据传输稳定性(必须的保证信息发送到一台主机,由该主机确认之后才能发送下一条信息),另外该协议也能保证数据传输的有序性(先发送的信息一定先到达)。一般基于C/S架构,存在服务器客户端模式。

应用领域: 语音通话,视频会议

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJfibHpj-1595765368926)(D:\带班资料\2020\j2003\线下\part1-JavaSE\20200721\笔记\assets\1595300606777.png)]

UDP协议(发快递)

User Diagram Protocol(用户数据报协议),是一个不安全的网络协议,不需要双方之间建立联系,也不保证信息传输的有序性(有可能后发消息先到),传输效率比TCP/IP更高.没有专门服务器和客户端,只有发送端和接收端

应用领域: 邮件发送,广播、飞秋

以上是互联网数据传输底层协议,目前大多数时候是直接使用的基于这两个协议的应用层协议

Http/Https、Ftp、Smtp、Pop3

IP与端口

ip(家庭住址)

Internet Protocol(因特网协议),主机之间通信的唯一标识,每台计算机都已个唯一的ip地址;ip又划分为IPv4和IPv6

  • IPv4由4个字节构成的一段地址(每个字节最大不能超过255,范围0~255,最大取值只能到40+亿个)
  • IPv6由16个字节构成

1字节=8个二进制位

4字节=32位

16字节=128位

ip地址需要确保在同一个网络中不可重复,一旦重复则会出现:ip冲突

ip地址通常分为5类:

A. (1.0.0.0 到127.0.0.0)

B. (128.1.0.0–191.254.0.0)

C. (192.0.1.0–223.255.254.0) 民用**

D. (224.0.0.0到239.255.255.255) 广播

E.(240.0.0.0到255.255.255.254,255.255.255.255)

端口(门牌号)

端口(port)是主机中应用程序对外的沟通的唯一标识;ip是主机的标识,端是应用的标识;因此如果需要准确的寻找到主机中的应用,则需要同时提供ip和端口。

端口分为TCP/IP、UDP

取值范围从 0 ~ 65535之间;但是由于0~1023之间的端口密集绑定了一些已有的服务,所以应该避免在创建自定义服务时使用这些端口;自定义的服务建议选择范围:

  • 1024~49151之间

域名

唯一的对ip地址简化记忆一种名称;例如:

#顶级域名
www.baidu.com
www.softeem.top
#二级域名
task.softeem.top
#三级
demo.task.softeem.top

域名后缀:

商用你:
.com
.cn
.net
个人组织:
.org
教育机构:
.edu
政府
.gov=

关于本机地址:

ip:127.0.0.1

域名:localhost

InetAddress类

InetAddress是位于java.net包中提供的用于表示ip地址和主机的类,常用方法:

  • getLocalhost() 获取本地主机
  • getByName() 根据提供的主机名或者ip获取InetAddress对象
  • getHostAddress() 获取主机地址
  • getHostName() 获取主机名称
public class InetAddressDemo {

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

        InetAddress ip = InetAddress.getLocalHost();
        System.out.println(ip);
        //根据主机名称获取包含了该主机的名称和地址的对象
        System.out.println(InetAddress.getByName("DESKTOP-UM5AJP5"));
        System.out.println(InetAddress.getByName("192.168.7.194"));

        //获取InetAddress对象表示的ip地址
        String addr = ip.getHostAddress();
        System.out.println(addr);
        String name = ip.getHostName();
        System.out.println(name);

        System.out.println(ip.getCanonicalHostName());
        System.out.println(InetAddress.getLoopbackAddress());

        byte[] byt = ip.getAddress();
        for (byte b : byt) {
            //-128 127  192/168
            System.out.println(b);
        }
    }

}

基于TCP/IP的Socket通信

​ Socket(套接字),实际上就是由IP地址跟端口号的结合,通过Socket对象可以实现两台主机之间的通信;Socket分为服务端Socket(java.net.ServerSocket),以及客户端Socket(java.net.Socket)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fUarnnoF-1595765368930)(D:\带班资料\2020\j2003\线下\part1-JavaSE\20200721\笔记\assets\1595339794776.png)]

服务端

/**
 * 服务器
 * @author mrchai
 *
 */
public class Server {

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

        //创建服务
        //		ServerSocket server = new ServerSocket();
        //		//将服务绑定到指定的主机以及指定端口
        //		server.bind(new InetSocketAddress(8888));

        //占据指定的端口创建一个服务(创办一家银行:还未开业)
        ServerSocket server = new ServerSocket(8888);
        System.out.println("服务已开启,等待连接...");
        while(true) {
            //开启监听
            Socket s = server.accept();
            InetAddress ip = s.getInetAddress();
            System.out.println(ip.getHostName()+"==="+ip.getHostAddress());


            //获取基于Socket的输出流
            OutputStream os = s.getOutputStream();
            //将节点流包装打印流
            PrintWriter out = new PrintWriter(os);
            out.println("欢迎使用SOFTEEM服务器!!!");
            out.flush();
            s.close();
        }
    }

}

客戶端

/**
 * 客户端
 * @author mrchai
 *
 */
public class Client {

    public static void main(String[] args) throws UnknownHostException, IOException {
        //连接到指定地址,指定端口的服务
        Socket s = new Socket("192.168.7.101",8888);

        //从socket中获取输入流
        InputStream is = s.getInputStream();
        //将字节流转换为字符流
        InputStreamReader isr = new InputStreamReader(is);
        //将字符节点流包装为缓冲字符输入流
        BufferedReader br = new BufferedReader(isr);
        //读取一行文本
        String msg = br.readLine();
        System.out.println("服务端:"+msg);
    }
}

综合案例:基于TCP/IP的文件服务器实现

服务端

public class FileServer {

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

        //准备需要传输的文件对象
        File target = new File("D:\\集团资料\\宣讲\\video\\云计算&大数据\\2017云栖.mp4");

        try(
            //创建并开启文件服务
            ServerSocket ss = new ServerSocket(6789);
            Socket s = ss.accept();
            //创建文件的输入流并包装为缓冲流
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(target));
            //获取基于Socket的输出流
            BufferedOutputStream bos = new BufferedOutputStream(s.getOutputStream());
        ){
            System.out.println("客户端连接:"+s.getInetAddress().getHostAddress()); 
            System.out.println("开始传输....");
            //每次读取的字节内容
            int b = 0;
            while((b = bis.read()) != -1) {
                bos.write(b);
            }
            System.out.println("传输完成");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端

public class FileClient {

    public static void main(String[] args) throws UnknownHostException, IOException {

        File f = new File("C:\\Users\\Administrator\\Desktop\\a.mp4");

        try(
            //连接指定ip指定端口服务
            Socket s = new Socket("192.168.7.141",6666);
            //获取基于Socket的输入流并包装为缓冲流
            BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
            //获取目标文件的输出流
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f));
        ){
            System.out.println("开始接收...");
            int b = 0;
            while((b = bis.read()) != -1) {
                bos.write(b);
            }
            System.out.println("接收完成!");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

}

基于多线程的文件服务器实现

由于上述的文件服务器为单线程的实现,因此只能服务于单个客户端,显然不符合实际需求;服务器的真正能力应该提供对于所有客户端的服务,因此,需要使得服务器能够对所有客户端提供服务就需要开启多线程支持:

public class FileServer2 extends Thread{

    private File source;
    private Socket s;

    public FileServer2(File source, Socket s) {
        super();
        this.source = source;
        this.s = s;
    }

    @Override
    public void run() {
        try {
            System.out.println("向"+s.getInetAddress().getHostAddress()+"传输....");
            //获取源文件的输入流
            InputStream in = new FileInputStream(source);
            //获取socket的输出流
            OutputStream out = s.getOutputStream();
            //传输
            TransferUtils.transfer(in, out);
            System.out.println(s.getInetAddress().getHostAddress()+"传输完成");
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                if(s != null)s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

        //在指定端口创建服务
        ServerSocket server =  new ServerSocket(6666);

        while(true) {
            //准备需要传输的文件对象
            File target = new File("D:\\集团资料\\宣讲\\video\\云计算&大数据\\2017云栖.mp4");
            Socket s = server.accept();
            System.out.println(s.getInetAddress().getHostAddress()+"进入服务器。。。");
            //创建文件传输的线程并启动
            new FileServer2(target, s).start();
        }
    }
}

另外针对于传输的功能封装了一个工具类:

TransferUtils
/**
 * 传输工具类
 * @author mrchai
 *
 */
public class TransferUtils {

	/**
	 * 将输入流的数据通过输出流输出
	 * @param in
	 * @param out
	 * @throws IOException
	 */
	public static void transfer(InputStream in,OutputStream out) throws IOException {
		try {
			byte[] b = new byte[1024];
			int len = 0;
			while((len = in.read(b)) != -1) {
				out.write(b, 0, len);
			}	
		}finally {
			if(out != null)out.close();
			if(in != null)in.close();
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值