ThreadLocal类
*
- ThreadLocal叫做线程变量,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程
- 读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
- ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法;如果每个线程
- 对变量的访问都是基于线程自己的变量这样就不会存在线程不安全问题。
- ThreadLocal和synchronized
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
- synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
- synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程
都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共
享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享
- 对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式。
前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影
响。
ThreadLocal应用场景
- 每个线程需要有自己单独的实例 - 实例需要在多个方法中共享,但不希望被多线程共享
ThreadLocal实现原理
早期版本的ThreadLocal可以理解为一个Map。当工作线程Thread实例向本地变量保持某个值时,
会以key-value形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保
存的值。当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其
绑定的Value。
JDK1.8每个线程拥有一个ThreadLocalMap,ThreadLocalMap中元素Entry由key-value对组成,
key为ThreadLocal对象,value为Object类型的值。Entry继承自WeakReference,并且在Entry
构造函数中,key也就是ThreadLocal被设置为弱引用,弱引用在垃圾回收时会被回收
- 每个Thread线程内部都有一个Map,即ThreadLocalMap
- Map里面存储ThreadLocal对象key和线程的变量副本value
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
- 对于不同的线程,每次获取副本值时,别的线程不能获取到当前线程的副本值,形成了副本的隔离
阻塞队列BlockingQueue
阻塞队列是Java5的内容,定义了阻塞队列的接口java.util.concurrent.BlockingQueue,
阻塞队列的概念是,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,
直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可
用元素为止
java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,
它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue
接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。不需要担心等待生产者
有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。
Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、
PriorityBlockingQueue,、SynchronousQueue等。
抛出异常(操作无法立即执行则抛出异常) | 特殊值(操作无法立即执行则返回一个特定值true / false) | 阻塞 | 超时 | |
插入 | add(e)在添加元素的时候,若超出了阻塞队列的长度会直接抛出异常 | offer(e)如果发现队列已满无法添加的话,会直接返回false | put(e)添加元素时发现队列已经满了会发生阻塞一直等待空间,以加入元素 | offer(e,time,unit) |
移除 | remove()若队列为空抛出NoSuchElementException异常 | poll()若队列为空,返回null | take()若队列为空,发生阻塞,等待有元素 | poll(time,unit) |
检查 | element() | peek() |
阻塞队列与平常接触的普通队列LinkedList或ArrayList等的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。
- 阻塞添加:所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作
- 阻塞删除:阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。
JAVA并发包提供三个常用的并发队列实现,分别是:ConcurrentLinkedQueue、LinkedBlockingQueue和
ArrayBlockingQueue。
使用阻塞队列实现的生产者消费者模式
阻塞的实现是依赖于阻塞队列实现的,put/take
测试类
BlockingQueue queue=new ArrayBlockingQueue<>(3);
new Producer(queue).start();
new Thread(new Consumer(queue)).start();
*
*
- BlockingQueue接口定义了一种阻塞的FIFO queue,每一个BlockingQueue都有一个容量,
- 让容量满时往BlockingQueue中添加数据时会造成阻塞,当容量为空时取元素操作会阻塞
-
- ArrayBlockingQueue是初始容器固定的阻塞队列,可以用来作为数据库模块成功竞拍的队
- 列,比如有10个商品,那么就设定一个10大小的数组队列。ArrayBlockingQueue的内部是通
- 过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞
-
- LinkedBlockingQueue是一个阻塞的线程安全的队列,底层采用链表实现,入队和出队都
- 用了加锁,当队空的时候线程会暂时阻塞;它如果不指定容量,默认为Integer.MAX_VALUE,
- 也就是无界队列。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用
- 的时候建议手动传一个队列的大小。
-
- put方法:1、队列已满,阻塞等待。2、队列未满,创建一个node节点放入队列中,如果放
- 完以后队列还有剩余空间,继续唤醒下一个添加线程进行添加。如果放之前队列中没有元素,
- 放完以后要唤醒消费线程进行消费
-
- take方法:1、队列为空,阻塞等待。2、队列不为空,从队首获取并移除一个元素,如果消
- 费后还有元素在队列中,继续唤醒下一个消费线程进行元素移除。如果放之前队列是满元素的
- 情况,移除完后要唤醒生产线程进行添加元素。
- ConcurrentLinkedQueue是一个基于链表的无界非阻塞队列,并且是线程安全的,它采用的是
- 先进先出的规则,当我们增加一个元素时,它会添加到队列的末尾,当我们取一个元素时,它
- 会返回一个队列头部的元素。不使用锁而是使用的是CAS原语无锁队列实现,是一个异步队列,
- 入队速度很快,出队进行了加锁,性能稍慢;当多个线程共享访问一个公共collection 时,
- ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素
两个常见的工具类
- Collections对集合提供一些常见的算法实现
- Arrays针对数组提供一些常见的算法实现
集合和數組之間的相互轉換
List list=new ArrayList();
Object[] arr=list.toArray(); 将集合转换为数组
Arrays.asList(arr):List 可以将一个数组装换为集合,务必注意这里返回的ArrayList不是java.util.ArrayList
- static sort(简单数值型数组) 排序
binarySearch用于实现在数组中进行折半查找,要求查找数据的数组必须有序
fill使用特定数据填充整个数组(每个数组的元素的值都是这个值)
copyOf数组复制,返回新数组
IO的面试
*
- AIO NIO和BIO的区别
BIO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成。 NIO:线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。 AIO:线程发起IO请求,立即返回;内存做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败。 BIO是一个连接一个线程。 NIO是一个请求一个线程。 AIO是一个有效请求一个线程。 BIO:同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。 NIO:同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。 AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
粘包处理
NIO Socket是非阻塞的通讯模式,与IO阻塞式的通讯不同点在于NIO的数据要通过Channel放到一个缓存池ByteBuffer中,
然后再从这个缓存池中读出数据,而 IO 的模式是直接从InputStream中read。所以对于NIO,由于存在缓存池的大小限制
和网速的不均匀会造成一次读的操作放入缓存池中的数据不完整,便形成了断包问题。同理,如果一次性读入两个及两个以
上的数据,则无法分辨两个数据包的界限问题,也就造成了粘包。
粘包问题主要是由于数据包界限不清,最好的解决办法就是在发送数据包前事先发送一个int型数据,该数据代表将要发送的
数据包的大小,这样接收端可以每次触发OP_READ的时候先接受一个int大小的数据段到缓存池中,然后,紧接着读出后续完
整的大小的包,这样就会处理掉粘包问题。因为channel.read()方法不能给读取数据的大小的参数,所以无法手动指定读取
数据段的大小。但每次调用channel.read()返回的是实际读取的大小。
- 常见的解决粘包、拆包的思路有:分隔符、固定消息长度、TLV格式消息等
解决思路:首先调整缓存池的大小固定为要读出数据段的大小,这样保证不会过量读出。由于OP_READ和OP_WRITE不是一一
对应的,所以一次OP_READ可以while循环调用channel.read()不停读取Channel中的数据到缓存池,并捕获其返回值,当
返回值累计达到要读取数据段大小时break掉循环,这样保证数据读取充足。所以这样就完美解决粘包问题。
UDP网络通信的发包过程
1、使用DatagramSocket()创建一个数据包套接字。
2、使用DatagramPacket(byte[]buf, int offset, int length, InetAddress address, int port)创建要发送的数据包。
3、使用DatagramSocket类的send()方法数据包
UDP网络通信的收包过程
1、使用DatagramSocket(int)创建一个数据包套接字,绑定到指定的端口。
2、使用DatagramPacket(byte[]buf,int length)创建字节数组来接收数据包.
3、使用DatagramSocket类的receive()方法接收UDP
获取的数据实际上就存储在创建空包的数组种,转换显示时建议设置长度
System.out.println(dp.getLength()); //数据的具体长度
String str=new String(buffer,0,dp.getLength());
一般来说UDP协议的最大数据包的长度64k
多播或者组播的实现
- 真正聊天室的实现原理
- MulticastSocket可以将数据报以广播的方式发送给加入指定组的所有客户端
- 组播是指把信息同时传递给一组目的地址。它使用的策略是最高效的,因为消息在每条网络链路上只需传递一次,且
- 只有在链路分叉的时候,消息才会被复制。与多播相比,常规的点到单点传递被称作单播。当以单播的形式把消息传
- 递给多个接收方时,必须向每个接收者都发送一份数据副本。由此产生的多余副本将导致发送方效率低下,且缺乏可
- 扩展性。不过,许多流行的协议——例如XMPP,用限制接收者数量的方法弥补了这一不足
代理服务器的使用
- JDK1.5提供了Proxy和ProxySelector类来实现代理访问。
- Proxy代表一个代理服务器,可以在打开URLConnection连接时指定代理,也可以在创建Socket连接时指定代理。
- ProxySelector是一个代理选择器,提供了对代理服务器更加灵活的控制,可以对http\https\ftp\socket等进
- 行分别设置,还可以设置不需要通过代理服务器的主机和地址
- 代理服务器的功能
-
- 突破自身IP限制
-
- 提高访问速度