1 JUC并发包
JUC 即 java.util.concurrent
类的简称。主要为并发编程提供了许多通用工具类。
2 线程的 ThreadLocal 本地缓存对象
线程范围内的共享变量,每个线程只能访问自己的数据,而不能访问其它线程数据。
每个线程调用全局 ThreadLocal
对象的 set
方法,相当于往其内部的 map 中增加一条记录,key 分别是各自的线程,value 是各自的set方法传进去的值。
2.1 创建线程类及 ThreadLocal 对象
import java.util.Random;
public class UserRunn implements Runnable {
//声明一个本地变量
private ThreadLocal<Person> userThreadLocal = new ThreadLocal<>();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 进入到run方法");
Person person = getPerson();
int age = new Random().nextInt(100);
System.out.println(Thread.currentThread().getName() + " 产生的年龄是:" + age);
System.out.println(Thread.currentThread().getName() + " 设置的年龄之前是" + person.getAge() + " 内存地址是" + person);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
person.setAge(age);
System.out.println(Thread.currentThread().getName() + " 设置的年龄之后是" + person.getAge() + " 内存地址是" + person);
}
public Person getPerson(){
Person u = userThreadLocal.get();
if(u == null){
u = new Person();
userThreadLocal.set(u);
}
return u;
}
}
class Person{
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2.2 创建测试类
public class Test {
public static void main(String[] args) {
UserRunn userRunn = new UserRunn();
new Thread(userRunn).start();
new Thread(userRunn).start();
}
}
2.3 运行后输出如下:
Thread-0 进入到run方法
Thread-1 进入到run方法
Thread-0 产生的年龄是:88
Thread-0 设置的年龄之前是0 内存地址是com.thread3.Person@6e0cc262
Thread-1 产生的年龄是:45
Thread-1 设置的年龄之前是0 内存地址是com.thread3.Person@6fec3797
Thread-1 设置的年龄之后是45 内存地址是com.thread3.Person@6fec3797
Thread-0 设置的年龄之后是88 内存地址是com.thread3.Person@6e0cc262
两个线程虽使用了同一个 Runnable 实体类进行了初始化,
但因为使用了 ThreadLocal 对象,对不同线程间的数据,进行了隔离,
因此可以看到,两个线程的数据彼此毫无关联。
3 线程的 volatile
关键字
volatile
关键字可以用来修饰字段(成员变量),作用是告诉程序,任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
3.1 创建线程类
当标识符未发生变化时,线程将进入死循环
public class UserThread extends Thread {
private volatile boolean flag = true;
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程正在运行");
while (flag){
}
System.out.println(Thread.currentThread().getName() + "线程运行结束");
}
}
3.2 创建测试类
在主线程中修改线程类的循环标识符
public class Test {
public static void main(String[] args) {
UserThread userThread = new UserThread();
userThread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//修改子线程死循环的标识位,终止循环
userThread.setFlag(false);
}
}
3.3 执行输出
Thread-0线程正在运行
Thread-0线程运行结束
volatile
的作用:使变量在多个线程间可见,但是未对数据加锁,无法保证数据的原子性或是一致性。
需要注意的是,一般 volatile
用于只针对多个线程可见的变量操作,并不能代替 synchronized
的同步功能。
3.4 验证 volatile 与 synchronized 的区别
public class VolatileThread extends Thread {
private static volatile int m = 0;
@Override
public void run() {
for(int i=0;i<10;i++){
m = m + 1;
try {
//增加随机性
sleep(3);
}catch (Exception e){
}
}
}
public static void main(String[] args) throws Exception{
//初始化线程
VolatileThread[] volatileThreads = new VolatileThread[100];
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i] = new VolatileThread();
}
//启动线程
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i].start();
}
//合并线程到主线程
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i].join();
}
System.out.println("m=" + VolatileThread.m);
}
}
执行结果:
始终小于1000
修改为 synchronized 同步代码块
public class VolatileThread extends Thread {
private static volatile int m = 0;
private static synchronized void mplus() {
m = m + 1;
}
@Override
public void run() {
for(int i=0;i<10;i++){
mplus();
try {
//增加随机性
sleep(3);
}catch (Exception e){
}
}
}
public static void main(String[] args) throws Exception{
//初始化线程
VolatileThread[] volatileThreads = new VolatileThread[100];
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i] = new VolatileThread();
}
//启动线程
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i].start();
}
//合并线程到主线程
for(int i =0;i<volatileThreads.length;i++){
volatileThreads[i].join();
}
System.out.println("m=" + VolatileThread.m);
}
}
执行结果:
m=1000
4 线程池的作用和应用
4.1 线程池的作用
通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
当任务到达时,任务可以不需要等到线程创建就能立即执行。
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
4.2 线程池的应用
场景:请求频繁,考虑到服务的并发问题,如果每个请求到来后,服务都为它启动一个线程,对于服务的资源可能会造成很大的浪费。
4.2.1 创建线程资源
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.UUID;
public class WorkThread extends Thread {
private Socket socket;
public WorkThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
//服务器接收客户端消息
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String in = br.readLine();
System.out.println(Thread.currentThread().getName() + " 服务器接收客户端的消息为: " + in);
//服务器向客户端发送消息
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("消息已收到" + UUID.randomUUID());
pw.flush();//立即发送
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.2.2 创建客户端
import java.io.*;
import java.net.Socket;
public class Client {
Socket socket;
public Client(){
try {
socket = new Socket("127.0.0.1", 8888);
System.out.println("客户端和服务器建立连接成功");
System.out.println("请客户端在控制台发送消息");
//客户端构建消息
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine();
//客户端发送消息
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println(line);
pw.flush();//立即发送消息
//接收消息
BufferedReader br1 = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String in = br1.readLine();
System.out.println("客户端接收的消息为: " + in);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Client();
}
}
4.2.3 创建服务端
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server {
private Socket socket;
private ServerSocket serverSocket;
public Server(){
System.out.println("服务器启动");
try {
serverSocket = new ServerSocket(8888);
ExecutorService executorService = Executors.newFixedThreadPool(3);
while (true){
socket = serverSocket.accept();
//每个客户端访问都会创建一个新的线程
//new WorkThread(socket).start();
//使用线程池,可以对线程进行复用
executorService.execute(new WorkThread(socket));
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Server();
}
}
其中在 Server 中的 new WorkThread(socket).start();
写法会导致每一个连接创建一个线程,最终将导致服务器资源枯竭。
而采用线程池,可以很好地规避这个问题。