线程并发问题(线程安全)
案例场景
- 银行取钱问题
- 买票问题
- 限时秒杀
线程安全
多线程并发时,多个线程同时操作同一个内存区域(变量),可能会导致的结果不一致的问题;所谓线程安全,指的是在多线程并发操作的时候能保证共享的变量数据一致
并发编程三要素
线程并发时需要保证线程安全,需要满足三大条件:
- 原子性
- 可见性
- 有序性
原子性(Atomicity)
对于一条线程执行来说要保证,要么都成功,要么都失败;对于原子性操作可以通过加锁的方式实现;Synchronized和ReentrantLock保证线程的原子性
可见性(Visibility)
多条线程操作同一个变量时,需要保证其中一条线程对变量的修改,对其他线程也是可见的
有序性(Ordering)
对于每一条线程的执行,内部应该是有序的:
- 代码顺序执行
- 锁的释放应该在获得之后
- 变量读取操作,在修改操作之后
线程同步解决方案
Synchronized
synchronized的关键字使用包含三种方式:
- synchronized块:对象锁
- synchronized方法:在方法声明的时候使用
- 类锁:在静态方法或者同步块中使用类的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();
}
}
注意事项:
- 锁的获取位置一般在try语句块之前
- 锁的释放一般放到finally语句块中
ReentrantLock和Synchronized的区别:
- ReentrantLock是一个实现类,后者是一个关键字
- ReentracntLock需要手动获取锁以及释放锁(粒度控制更细);后者自动加锁,临界区(synchronized锁住的区域)的代码执行完之后自动释放
- ReentrantLock支持公平锁以及非公平锁;sychronized只支持非公平锁
- 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¬ify
线程运行时若需要限时等待,则可以通过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)
网络模型与协议
计算机网络之间实现通信需要两种设备支持:
- 硬件设备:(网卡,网关:交换机,路由器)
- 软件设备:(网络通信协议: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();
}
}
}