原子性
强调一组操作的整体性,这一组操作不可分割,要么同时成功,要么同时失败。不能让其他操作打断。
volatile关键字不能保证数据操作的原子性的,但是加同步锁可以保证原子性,相对应的性能就会降低。
原子类底层实现
CAS算法(内存值,读取的旧值,要替换的新值)
该算法会判断旧值是否和内存值相等,如果相等则使用新值替换内存值。如果旧值和内存值不等,说明我们在操作的过程中,有其他线程插入做了数据的修改操作,此时我们的操作应该是要放弃的。我们只能从头再操作一遍。获取内存中的最新值,作为旧值存储,然后再继续计算出新值,然后再用CAS算法判断一次....
悲观锁和乐观锁
悲观锁和乐观锁是看待多线程问题解决时候的两种不同的解决思想。
悲观锁:每一次操作都悲观的认为操作肯定会被其他线程干扰,所以一定要在操作语句块中添加锁,同一时间只能一个线程执行。所以效率比较低。
乐观锁:每一次操作都乐段的认为操作不会被其他线程干扰,所以不添加任何锁的机制。而是在最终更新数据前做一次校验,校验成功则直接执行完毕;校验不成功则重新执行一次。
HashTable
是HashMap的改造版本,主要功能没有任何区别。只不过把所有的方法变成了同步方法。所以是线程安全的。
ConcurrentHashMap
HashTable也是线程安全的,但是它的同步是用syncronnized关键字把所有方法统一进行了加锁。在多线程模型下,HashTable所有操作使用同一把锁,锁住整张hash表,效率极其低。
JDK7及之前:
ConcurrentHashMap使用了分段锁的思想,首先创建一个数组作为第一次hash的查表数组。该数组中存的不是元素的值,而是另一张hash表,元素需要二次哈希,把自己使用和之前HashMap算法一致的算法进行存储。外部的大数组,每个元素都是一个单独的锁。当操作某个元素所对应的小hash表的时候,仅仅锁住当前的小表。其他的hash表依然可以并发被操作。
JDK8及以后
ConcurrentHashMap放弃了分段数组,还是只使用一个数组作为hash表。
分为以下两种情况:
存元素的时候该位置没有值,此时使用乐观锁CAS算法添加第一个节点。
如果存元素的时候该位置已经有值了,此时使用悲观锁syncronized代码块使用头结点当做锁锁住当前链表或者红黑树。
网络编程
三要素
IP地址:在网络中唯一确定一台机器的地址。
ip:一组数字,用于唯一表示一个机器的地址。
ipv4(主流)
ipv6
域名:用人类可以方便记忆的字符串,代替ip的数字,便于使用。域名和ip的对应关系,会绑定在DNS(域名解析服务器)上。
端口号:在一台机器中,唯一确定一个应用程序的数字。
0-65535,其中1024以内作为保留端口,一般不要使用。
协议:就是我们数据传输的方式和规则。UDP和TCP
命令:
ipconfig 查看当前IP地址
ping 域名或ip 计算机会模拟一些数据发送给指定的目标地址,如果能发送过去并接收到对方的回应,则代表网络畅通。
本机回环地址:127.0.0.1 无论当前计算机的ip是什么,我们访问该地址都是访问本机。
netstat -ano | findstr 端口号 查找某个端口被哪个进程占用了,获取到的是一个pid,也就是进程id。我们可以使用任务管理器强制关闭它。
InetAddress
IP地址对应的对象。
协议
UDP和TCP
UDP是无连接协议速度比较快,但是不安全,数据可能丢失;TCP是有连接的协议,速度略慢,但是安全,数据不会丢失,数据没有大小限制。
UDP协议通信
DatagramSocket
使用UDP协议发送或接收数据的核心类。
如果是接收的话,构造方法不要使用空参的。必须手动指定某个端口,从此端口接收。
send(DatagramPacket dp)方法,发送指定的数据包
close() 释放资源
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
数据包,内部封装了要发送的数据、数据的长度、目标的IP地址和端口号
发送端
//1.找码头
DatagramSocket ds = new DatagramSocket();
//2.打包礼物
//DatagramPacket(byte[] buf, int length, InetAddress address, int port)
String s = "送给村长老丈人的礼物";
byte[] bytes = s.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10000;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);
//3.由码头发送包裹
ds.send(dp);
//4.付钱走羊
ds.close();
接收端
//1.找码头 ---- 表示接收端从10000端口接收数据的.
DatagramSocket ds = new DatagramSocket(10000);
//2,创建一个新的箱子
byte [] bytes = new byte[1024 * 64];
// 期望接收的数据的大小,但是实际不一定是这么大
DatagramPacket dp = new DatagramPacket(bytes,bytes.length);
//3.接收礼物,把礼物放到新的箱子中
System.out.println("-----------接收前----------");
ds.receive(dp);
// 阻塞式方法
System.out.println("------------接收后---------");
//4.从新的箱子里面获取礼物//
byte[] data = dp.getData();
int length = dp.getLength();
// 实际传递过来的数据的长度
System.out.println(new String(bytes,0,length));
//5.拿完走羊
ds.close();
UDP的通信方式
单播:只有一个接收端。
组(多)播:有多个接收端,把接收端建立一个小组进行接收。
-
发送的时候的ip需要特殊指定,指定组播的ip发送。
2. 接收端,核心对象不再是DatagramSocket,改为MulticastSocket 3. 接收端,MulticastSocket必须调用joinGroup(InetAddress ip),加入到指定的组播的ip中去。
广播:当前局域网内所有人。
相当于给所有人发送了一次单播,所以代码和单播的代码一致,只不过发送端的目标ip比较特殊,是广播ip--255.255.255.255
思考题:
使用组播的方式,完善我们的聊天室案例。
TCP
tcp协议要顺利完成通信,必须要保证客户端和服务器端能成功连接。
客户端
//1,创建一个Socket对象 要和目标服务器建立连接
Socket socket = new Socket("127.0.0.1",10000);
//2.获取一个IO流开始写数据 这个流就是连接的客户端和服务器,通过网络传输数据
OutputStream os = socket.getOutputStream();
os.write("hello".getBytes());
//3.释放资源
os.close();
socket.close();
服务器端
//1. 创建Socket对象 监听10000端口,客户端就会连这个端口
ServerSocket ss = new ServerSocket(10000);
//2. 等待客户端连接,这是一个阻塞式的方法,如果获取不到客户端的连接,则一直在这等待。每次获取一个客户端的连接,该方法每调用一次就会接收一个客户端的连接
Socket accept = ss.accept();
//3.获得输入流对象,该输入流对接的就是客户端的输出流
InputStream is = accept.getInputStream();
int b;
while((b = is.read()) != -1){
System.out.print((char) b);
}
//4.释放资源
is.close();
ss.close();
注意:在用完流和Socket之后,一定记得释放资源。也就是调用close方法。
三次握手
作用:保证客户端和服务器能成功建立连接。
-
客户端给服务器发送连接的请求
-
服务器返回给客户端可以允许它连接的响应
-
客户端给服务器发送表示已经收到服务器的许可,真正要建立连接了
四次挥手
作用;保证客户端和服务器能成功断开连接
-
客户端给服务器发送断开连接的请求
-
服务器返回给客户端要等一下,把最后的一点数据处理完毕后我通知你你再断开
-
服务器处理完毕后,通知客户端,说可以允许你断开了
-
客户端通知服务器表示收到断开的许可,真正断开连接
Socket
该对象可以通过指定的ip和端口建立连接。
该对象可以获取输入流或者输出流,在网络中传输数据。
注意:在Socket流操作完毕之后,要记得手动把流关闭一下。
思考:
编写案例,跟小组内其他成员配合进行文件传递。传递完成后,把“文件名+传送完毕”返回回来。
提示:可以使用对象操作流考虑实现。
UUID
randomUUID()
随机生成一个唯一不重复的字符串。
多线程优化
-
如果我们的服务器使用单线程模型运行,接收到一个客户端的连接之后,只能执行完它的所有任务才能接收下一个。如果任务执行的时间比较长,会导致客户端等待时间比较长。客户端之间不能并发,效率比较低。
-
要解决上述问题,我们引入多线程模型。让主线程仅仅用于循环接收跟客户端的连接,一旦确定连接。主线程不再执行任务,而是让新的线程执行。主线程继续获取连接。此时,客户端的任务之间是并发的。新的问题:每个任务都创建新的线程,会导致系统资源浪费严重,甚至导致系统崩溃。
-
要解决上述问题,我们引入线程池,把线程利用率提升上来,尽可能的循环利用线程执行逻辑。