同步的静态方法在Java中通过static synchronized
声明,这意味着该方法对类的所有实例共享一个锁。只有当一个线程持有类的锁时,其他线程才能访问该方法。
class Boo {
public static synchronized void doSome() {
System.out.println(Thread.currentThread().getName() + " is executing doSome()");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished executing doSome()");
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(Boo::doSome, "Thread 1");
Thread t2 = new Thread(Boo::doSome, "Thread 2");
t1.start();
t2.start();
}
}
doSome()
是一个静态同步方法,t1
和t2
线程在执行时会串行化,只有当一个线程完成后,另一个线程才能进入该方法。静态方法使用同步块是为了对部分代码块进行同步,而不是对整个方法进行同步。这样可以提高效率,因为只有关键部分需要锁定,而其他部分可以并发执行。
class Boo {
public static void doSome() {
System.out.println(Thread.currentThread().getName() + " is entering doSome()");
synchronized (Boo.class) { // 锁定Boo类对象
System.out.println(Thread.currentThread().getName() + " is executing synchronized block");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished synchronized block");
}
System.out.println(Thread.currentThread().getName() + " is exiting doSome()");
}
}
public class Main {
public static void main(String[] args) {
Thread t1 = new Thread(Boo::doSome, "Thread 1");
Thread t2 = new Thread(Boo::doSome, "Thread 2");
t1.start();
t2.start();
}
}
在上面的代码中,Thread 1
和Thread 2
可以同时进入doSome()
方法的同步块外的部分,但只能一个线程进入同步块,确保了同步块内的操作是线程安全的。
锁定对象:synchronized (Boo.class)
语句锁定了Boo
类的Class对象。由于这是一个静态方法,锁定的是整个类,而非某个实例。
同步块效果:当一个线程进入同步块时,其他线程需要等待该线程释放锁才能进入同步块。但同步块外的代码可以并发执行。
提高效率:如果方法中的其他部分不需要同步,可以只对关键部分加锁,减少等待时间,提高并发性能。
互斥锁是什么:互斥锁(Mutex)是指几个线程执行几个不一样的代码,但是代码又不能一起执行,使用互斥锁;
如何实现互斥锁:在Java中,通过synchronized
关键字为像创建互斥锁的两端代码加上本关键字来实现互斥锁,可以用在方法或同步块上。
package day20;
public class MutexDemo {
public static void main(String[] args) {
Aoo aoo = new Aoo();
new Thread(aoo::methodA).start();
new Thread(aoo::methodB).start();
}
}
class Aoo{
public void methodA() {
synchronized (this) {
Thread thread = new Thread();
System.out.println(thread.getName() + ":methodA");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + ":methodA完毕");
}
}
public void methodB() {
synchronized (this) {
Thread thread = new Thread();
System.out.println(thread.getName() + ":methodB");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName() + ":methodB完毕");
}
}
}
定义:StringBuilder
是一个可变的字符序列,可以在不生成新的对象的情况下对字符串进行修改。
线程安全性:StringBuilder
不是线程安全的。它在单线程环境中表现更好,因为它不需要在每个方法调用时处理同步,因此速度较快。
性能:由于没有同步机制,StringBuilder
通常比StringBuffer
更快,在没有并发访问的情况下,StringBuilder
是更好的选择。
定义:StringBuffer
也是一个可变的字符序列,与StringBuilder
类似,但它在每个方法上都进行了同步。
线程安全性:StringBuffer
是线程安全的。它通过在方法上加锁,确保多线程环境下的安全操作。多个线程可以同时访问StringBuffer
对象,而不会导致数据不一致。
性能:由于每次操作都要进行同步,StringBuffer
的性能会比StringBuilder
稍低。但在需要线程安全的环境中,StringBuffer
是更好的选择。
单线程环境:StringBuilder
更适合,因为它更快。
多线程环境:StringBuffer
更适合,因为它是线程安全的。
package day20;
/**
* StringBuilder不是并发安全的,不能在多线程场景下使用(不能同时被多个线程操作)
* StringBuffer是并发安全的,支持多个线程同时操作
*/
public class SyncAPIDemo1 {
public static void main(String[] args) {
// StringBuilder builder = new StringBuilder();
StringBuffer builder = new StringBuffer();
Thread t1 = new Thread(){
public void run(){
for (int i = 0; i < 1000; i++) {
builder.append("a");
}
}
};
Thread t2 = new Thread(){
public void run(){
for (int i = 0; i < 1000; i++) {
builder.append("b");
}
}
};
t1.start();
t2.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(builder.length());
}
}
ArrayList
LinkedList
HashSet
这些集合类在默认情况下都不是线程安全的,多线程并发操作可能会导致数据不一致或异常。
静态方法:Collections.synchronizedXXX()
Collections.synchronizedList()
、Collections.synchronizedSet()
。 package day20;
import java.util.*;
/**
* 集合实现:
* ArrayList,LinkedList,HashSet他们都不是并发安全的!
*/
public class SyncAPIDemo2 {
public static void main(String[] args) {
// List c = new ArrayList<>();
// List c = new LinkedList<>();
// Collections可以将现有的集合转换为并发安全的
// List c = Collections.synchronizedList(new ArrayList<>());
// List c = Collections.synchronizedList(new LinkedList<>());
Set<Integer> c = Collections.synchronizedSet(new HashSet<>());
Thread t1 = new Thread(){
public void run(){
for (int i = 0; i < 1000; i++) {
c.add(i);
}
}
};
Thread t2 = new Thread(){
public void run(){
for (int i = 1000; i < 2000; i++) {
c.add(i);
}
}
};
t1.start();
t2.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
}
System.out.println(c.size());
}
}
Client.java
package day20;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 客户端
*/
public class Client {
/**
* java.net.Socket:套接字
* 它封装了TCP协议的通信细节,使用它可以和服务器建立连接并进行交互
* 可以理解为是电话
*/
private Socket socket;
// 处理回复消息,创建回复接口:该线程任务负责读取来自服务端发送过来的消息并输出到控制台上
private class ServerHandler implements Runnable {
@Override
public void run() {
try {
// 通过Socket提供的方法InputStream getInputStream(),获取输入流,将服务端发送的消息读取出来
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 循环读取来自服务端发送过来的消息并输出到客户端控制台上
String message;
while ((message = bufferedReader.readLine()) != null) {
System.out.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public Client() {
// 实例化Socket对象,就是与远端计算机建立连接的过程
// 需要传递两个对数:
// 1.远端计算机的IP地址,用于找到网络上对方的计算机
// 2.远端计算机的端口号,用于找到对方计算机中的应用程序
try {
System.out.println("正在连接服务端......");
/**
* 如何查找本机IP地址:
* Windows:Win+R,输入cmd,回车,输入ipconfig,回车
* Mac:触控板上五指向中间抓,选择"终端"程序打开,输入/sbin/ifconfig查看IP地址
*/
socket = new Socket("localhost", 8888);
System.out.println("连接成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 该方法用于启动客户端程序的执行
public void start() {
// 通过Socket获取输出流用于向服务端发送消息
try {
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
PrintWriter printWriter = new PrintWriter(bufferedWriter,true); // 自动行刷新
// 输入昵称
Scanner scanner01 = new Scanner(System.in);
while (true) {
System.out.println("请输入昵称:");
// 要求:昵称不能为空或者只有空格都不可以,如果发生则提示用户重新输入
String nickname = scanner01.nextLine();
if (nickname.trim().isEmpty()){
System.out.println("昵称不能为空,请重新输入!");
}else {
printWriter.println(nickname);
System.out.println("昵称设置成功!欢迎您:" + nickname);
break;
}
}
// 启动用于读取服务端消息的线程
ServerHandler serverHandler = new ServerHandler();
Thread thread = new Thread(serverHandler);
// 如果我们输入exit想结束聊天,这个线程依然活着,所以我们想让他和主线程一起死,于是我们设计这个线程为守护线程
thread.setDaemon(true);
thread.start();
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("exit".equalsIgnoreCase(line)) {
break;
}
printWriter.println(line); // 发送消息给服务端
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
socket.close(); // 进行四次挥手
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
server.java
package day20;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* 服务端
*/
public class Server {
/**
* java.net.ServerSocket:
* 运行在服务端,主要作用有两个:
* 1.像系统申请服务端口,客户端通过该端口与服务端建立连接
* 2.监听服务端口,一旦有客户链接了,就会立即创建一个Socket对象与客户端进行交互
* 如果将Socket比喻为“电话”,那ServerSocket就相当于客户中心的“总机”。
* 解决方法:
* 1.更换端口号;
* 2.杀死占用该端口的进行(通常由于服务端启动了两次导致)
*/
private ServerSocket serverSocket;
private List<PrintWriter> allOut = new ArrayList<>();
private String nickname;
public Server(){
try {
System.out.println("正在启动服务端......");
serverSocket = new ServerSocket(8888);
System.out.println("服务端启动完成!");
} catch (IOException e) {
e.printStackTrace();
}
}
// 启动服务端方法:
/**
* accept():
* 用于接收客户端连接,并返回一个Socket对象与所连接的客户端进行交互
* 该方法是一个阻塞方法,调用后程序会“卡住”,直到一个客户端连接为止
*/
public void start(){
try {
while (true){
System.out.println("服务端正在等待客户端连接......");
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了!");
// 启动单独的线程来处理与客户端的通信
ClientHandler clientHandler = new ClientHandler(socket);
Thread thread = new Thread(clientHandler);
thread.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server();
server.start();
}
/**
* 该线程任务用于负责与指定的客户端通信
*/
private class ClientHandler implements Runnable{
private Socket socket;
private String host; // 记录客户端的IP地址
public ClientHandler(Socket socket) {
this.socket = socket;
// 通过socket获取客户端ip地址
host = socket.getInetAddress().getHostAddress();
}
@Override
public void run() {
PrintWriter printWriter = null;
try {
// 接收客户端发送过来的消息
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
// 读取客户端发送过来的第一行字符串一定是客户端的昵称
nickname = bufferedReader.readLine();
// 通过Socket获取输出流,用于将消息发送给该客户端
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
printWriter = new PrintWriter(bufferedWriter,true);
// 将对应该客户端的输出流存入到共享集合allOut中
synchronized (allOut) {
allOut.add(printWriter);
}
// 对应该客户端的输出流共享到allOut中
allOut.add(printWriter);
// 广播该客户端上线了
broadcast(nickname + "["+host+"]上线了!当前在线人数:" + allOut.size());
String message;
while ((message = bufferedReader.readLine()) != null) { // 读取客户端发送过来的消息
broadcast(host + "说:" + message);
}
} catch (IOException e) {
} finally {
// 处理该客户端断开连接后的操作
// 将该客户端的输出流从共享集合allOut中删除
synchronized (allOut) {
allOut.remove(printWriter);
}
// 广播消息,告知所有客户端该用户下线了
broadcast(host + "下线了!当前在线人数:" + allOut.size());
try { // 进行四次挥手
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void broadcast(String message){
System.out.println(message);
synchronized (allOut) {
for (PrintWriter writer : allOut) {
writer.println(message);
}
}
}
}
}