十一、java多线程机制
Java提供了内置的多线程支持。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
在Java中,有四种主要的实现多线程的方式:继承Thread类、实现Runnable接口、实现Callable接口通过FutureTask包装器来创建Thread线程、使用ExecutorService、Callable、Future实现有返回结果的多线程。
生命周期:
Java线程的生命周期包括五个状态:新建状态 (New)、就绪状态 (Runnable)、运行状态 (Running)、阻塞状态 (Blocked)和死亡状态 (Terminated)。
- **新建状态**:当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态。
- **就绪状态**:当线程对象调用了start()方法之后,该线程处于就绪状态。处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。
- **运行状态**:如果就绪状态的线程获取CPU资源,它就可以执行run()方法,此时线程便处于运行状态。
- **阻塞状态**:阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
- **死亡状态**:线程会以以下三种方式之一结束,结束后就处于死亡状态: run()方法执行完成,线程正常结束;线程抛出一个未捕获的Exception或Error;直接调用该线程的stop()方法来结束该线程。
下面是一个简单的例子,它演示了一个线程在其生命周期中经历的不同状态:
public class ThreadStateExample {
public static void main(String[] args) throws InterruptedException {
// 新建状态
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
// 阻塞状态
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
System.out.println("新建状态: " + thread.getState());
// 就绪状态
thread.start();
System.out.println("就绪状态: " + thread.getState());
// 运行状态
Thread.sleep(100);
System.out.println("运行状态: " + thread.getState());
// 阻塞状态
Thread.sleep(1000);
System.out.println("阻塞状态: " + thread.getState());
// 死亡状态
Thread.sleep(3000);
System.out.println("死亡状态: " + thread.getState());
}
}
这个程序创建了一个新线程,该线程在其run()方法中调用了Thread.sleep()方法,使其进入阻塞状态。在主线程中,我们使用Thread.sleep()方法来暂停主线程的执行,以便能够在不同时间点观察新线程的状态。
当你运行这个程序时,你会看到类似下面的输出:
新建状态: NEW
就绪状态: RUNNABLE
运行状态: TIMED_WAITING
阻塞状态: TIMED_WAITING
死亡状态: TERMINATED
`Thread t = Thread.currentThread()` 这行代码的作用是获取当前正在执行的线程对象,并将其赋值给变量`t`。
`Thread.currentThread()` 是一个静态方法,它返回对当前正在执行的线程对象的引用。这个方法常用于获取当前线程的状态或者名称等信息。
例如,下面的代码演示了如何使用 `currentThread()` 方法来获取当前线程的名称:
public class CurrentThreadExample {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("当前线程: " + t.getName());
}
}
当你运行这个程序时,它会输出类似下面的内容:
当前线程: main
垃圾实体:
Thread thread = new Thread(target);
thread.start();
然后再一次创建thread = new Thread(target);那么上面那个就会变成垃圾实体。又因为垃圾实体还在运行状态,它仍会继续工作。
线程同步:
线程同步是指在多线程环境下,对共享资源的访问进行协调,以防止多个线程同时访问共享资源导致数据不一致的问题。Java提供了多种同步机制来实现线程同步,包括`synchronized`关键字、`Lock`接口及其实现类、`Semaphore`类等。
下面是一个简单的例子,它演示了如何使用`synchronized`关键字来实现线程同步:
public class Bank implements Runnable {
int money = 200;
public static void main(String args[]) {
Bank bank = new Bank();
bank.setMoney(200);
Thread accountant, // 会计
cashier; // 出纳
accountant = new Thread(bank);
cashier = new Thread(bank);
accountant.setName("会计");
cashier.setName("出纳");
accountant.start();
cashier.start();
}
public void setMoney(int n) {
money = n;
}
public void run() {
if (Thread.currentThread().getName().equals("会计"))
saveOrTake(300);
else if (Thread.currentThread().getName().equals("出纳"))
saveOrTake(150);
;
}
//synchronized 这个同步类型不能少,因为运行有两个进程,如果没有同步机制,就会造成抢占。
public synchronized void saveOrTake(int amount) { // 存取方法
if (Thread.currentThread().getName().equals("会计")) {
for (int i = 1; i <= 3; i++) {
money = money + amount / 3; // 每存入amount/3,稍歇一下
System.out.println(Thread.currentThread().getName() + "存入"
+ amount / 3 + ",帐上有" + money + "万,休息一会再存");
try {
Thread.sleep(1000); // 这时出纳仍不能使用saveOrTake方法
} catch (InterruptedException e) {
}
}
} else if (Thread.currentThread().getName().equals("出纳")) {
for (int i = 1; i <= 3; i++) { // 出纳使用存取方法取出60
money = money - amount / 3; // 每取出amount/3,稍歇一下
System.out.println(Thread.currentThread().getName() + "取出"
+ amount / 3 + "帐上有" + money + "万,休息一会再取");
try {
Thread.sleep(1000); // 这时会计仍不能使用saveOrTake方法
} catch (InterruptedException e) {
}
}
}
}
}
等待wait(),只存在同步机制里。
同步机制,也需要一个很重要的思想:等待wait() 和 通知等待结束notify()
public class TicketHouse implements Runnable {
public static void main(String args[]) {
TicketHouse officer = new TicketHouse();
Thread zhangfei, likui;
zhangfei = new Thread(officer);
zhangfei.setName("张飞");
likui = new Thread(officer);
likui.setName("李逵");
zhangfei.start();
likui.start();
}
int fiveAmount = 2, tenAmount = 0, twentyAmount = 0;
public void run() {
if (Thread.currentThread().getName().equals("张飞")) {
saleTicket(20);
} else if (Thread.currentThread().getName().equals("李逵")) {
saleTicket(5);
}
}
private synchronized void saleTicket(int money) {
if (money == 5) { // 如果使用该方法的线程传递的参数是5,就不用等待
fiveAmount = fiveAmount + 1;
System.out.println("给" + Thread.currentThread().getName() + "入场卷,"
+ Thread.currentThread().getName() + "的钱正好");
} else if (money == 20) {
while (fiveAmount < 3) {
try {
System.out.println("\n" + Thread.currentThread().getName()
+ "靠边等...");
wait(); // 如果使用该方法的线程传递的参数是20须等待
System.out.println("\n" + Thread.currentThread().getName()
+ "继续买票");
} catch (InterruptedException e) {
}
}
fiveAmount = fiveAmount - 3;
twentyAmount = twentyAmount + 1;
System.out.println("给" + Thread.currentThread().getName() + "入场卷,"
+ Thread.currentThread().getName() + "给20,找赎15元");
}
notifyAll();
}
}
线程联合:join方法
大概就是有两个线程A,B,在A线程里面使用线程B对象的join方法,则会从这里转到线程B去运行线程B,而A等待线程B执行结束。
和wait有点像,但不一样。它不需要使用同步机制,就能实现等待。
GUI线程:
GUI线程(Graphical User Interface Thread)是指专门用来处理图形用户界面的线程。它负责监听用户在图形用户界面上的操作,并对这些操作进行响应。在许多图形用户界面应用程序中,GUI线程通常是主线程。
GUI线程负责维护消息队列,接收和分发来自操作系统或其他应用程序的消息。例如,当用户点击鼠标或按下键盘上的某个键时,操作系统会将相应的消息发送到应用程序的消息队列中。GUI线程会从消息队列中取出这些消息,并将它们分发给相应的窗口或控件进行处理。
例如:打字母游戏,使用线程:计时操作。监听:输入字母。
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class WindowTyped extends JFrame implements ActionListener, Runnable {
public static void main(String args[]) {
WindowTyped win = new WindowTyped();
win.setTitle("打字母游戏");
win.setSleepTime(3000);
}
JTextField inputLetter;
Thread giveLetter; // 负责给出字母
JLabel showLetter, showScore;
int sleepTime, score;
Color c;
WindowTyped() {
setLayout(new FlowLayout());
giveLetter = new Thread(this);
inputLetter = new JTextField(6);
showLetter = new JLabel(" ", JLabel.CENTER);
showScore = new JLabel("分数:");
showLetter.setFont(new Font("Arial", Font.BOLD, 22));
add(new JLabel("显示字母:"));
add(showLetter);
add(new JLabel("输入所显示的字母(回车)"));
add(inputLetter);
add(showScore);
inputLetter.addActionListener(this);
setBounds(100, 100, 400, 280);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
giveLetter.start(); // 在AWT-Windows线程中启动giveLetter线程
}
public void run() {
char c = 'a';
while (true) {
showLetter.setText("" + c + " ");
validate();
c = (char) (c + 1);
if (c > 'z')
c = 'a';
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
}
}
}
public void setSleepTime(int n) {
sleepTime = n;
}
public void actionPerformed(ActionEvent e) {
String s = showLetter.getText().trim();
String letter = inputLetter.getText().trim();
if (s.equals(letter)) {
score++;
showScore.setText("得分" + score);
inputLetter.setText(null);
validate();
//如果没有结束休眠操作,那么输出回车,字母不会发生改变
giveLetter.interrupt(); // 吵醒休眠的线程,以便加快出字母的速度
}
}
}
计时器:Timer类
应该不属于线程,只是有点像,
应用举例:记时答题小程序
`FileReader` 类是 Java 中用于读取字符文件的便捷类。它继承自 `InputStreamReader` 类,可以将文件中的字节转换为字符。`FileReader` 类的构造函数接受一个 `File` 对象或一个文件名字符串作为参数,用于指定要读取的文件。
`BufferedReader` 类是 Java 中用于读取文本的缓冲字符输入流。它继承自 `Reader` 类,可以将文本从字符输入流中高效地读取到字符数组中。`BufferedReader` 类的构造函数接受一个 `Reader` 对象作为参数,可以使用 `FileReader` 对象来创建 `BufferedReader` 对象。
下面是一个简单的例子,它演示了如何使用 `FileReader` 和 `BufferedReader` 类来读取文本文件中的内容:
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try {
FileReader fileReader = new FileReader("example.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这个程序使用 `FileReader` 类来打开名为 "example.txt" 的文本文件,并使用 `BufferedReader` 类来从文件中读取文本。当读取到文件末尾时,`readLine()` 方法会返回 `null`,循环结束。
解决文件中有中文,最后出现乱码的问题:
乱码问题通常是由于编码不一致导致的。在你的例子中,`FileReader` 类使用默认的字符编码来读取文件,如果文件的编码与默认编码不同,就可能出现乱码问题。
要解决这个问题,你可以使用 `InputStreamReader` 类来代替 `FileReader` 类,并在创建 `InputStreamReader` 对象时指定文件的编码。例如,如果你的文件是以 "UTF-8" 编码保存的,你可以这样修改你的代码:
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
try {
FileInputStream fileInputStream = new FileInputStream("D:\\桌面\\例子代码\\chapter12\\例子14\\test.txt"); //输入流的类
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8"); //缓冲流的类
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个修改后的例子中,我们使用 `FileInputStream` 类来打开文件,并使用 `InputStreamReader` 类来读取文件中的字节并将它们转换为字符。在创建 `InputStreamReader` 对象时,我们指定了文件的编码为 "UTF-8"。
答题小程序代码:
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class StandardExamInTime extends JFrame implements ActionListener,
ItemListener {
public static void main(String args[]) {
StandardExamInTime win = new StandardExamInTime();
win.setTitle("限时回答问题");
win.setTestFile(new java.io.File("D:\\桌面\\例子代码\\chapter12\\例子14\\test.txt"));
win.setMAX(8);
}
File testFile;
int MAX = 8;
int maxTime = MAX, score = 0;
javax.swing.Timer time; // 计时器
JTextArea showQuesion; // 显示试题
JCheckBox choiceA, choiceB, choiceC, choiceD;
JLabel showScore, showTime;
String correctAnswer; // 正确答案
JButton reStart;
// FileReader inOne;
BufferedReader inTwo;
StandardExamInTime() {
time = new javax.swing.Timer(1000, this);
showQuesion = new JTextArea(2, 16);
setLayout(new FlowLayout());
showScore = new JLabel("分数" + score);
showTime = new JLabel(" ");
add(showTime);
add(new JLabel("问题:"));
add(showQuesion);
choiceA = new JCheckBox("A");
choiceB = new JCheckBox("B");
choiceC = new JCheckBox("C");
choiceD = new JCheckBox("D");
choiceA.addItemListener(this);
choiceB.addItemListener(this);
choiceC.addItemListener(this);
choiceD.addItemListener(this);
add(choiceA);
add(choiceB);
add(choiceC);
add(choiceD);
add(showScore);
reStart = new JButton("再做一遍");
reStart.setEnabled(false);
add(reStart);
reStart.addActionListener(this);
setBounds(100, 100, 200, 200);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public void setMAX(int n) {
MAX = n;
}
public void setTestFile(File f) {
testFile = f;
score = 0;
try {
// inOne = new FileReader(testFile);
//下面这两个代替一个是输入流,一个是缓存流。用来代替前面的FileReader,防止中文乱码的问题。
FileInputStream file = new FileInputStream(testFile);
InputStreamReader inOne = new InputStreamReader(file,"UTF-8");
inTwo = new BufferedReader(inOne);
readOneQuesion();
reStart.setEnabled(false);
} catch (IOException exp) {
showQuesion.setText("没有选题");
}
}
public void readOneQuesion() {
showQuesion.setText(null);
try {
String s = null;
while ((s = inTwo.readLine()) != null) {
if (!s.startsWith("-"))
showQuesion.append("\n" + s);
else {
s = s.replaceAll("-", "");
correctAnswer = s;
break;
}
}
time.start(); // 启动计时
if (s == null) {
inTwo.close();
reStart.setEnabled(true);
showQuesion.setText("题目完毕");
time.stop();
}
} catch (IOException exp) {
}
}
public void itemStateChanged(ItemEvent e) {
JCheckBox box = (JCheckBox) e.getSource();
String str = box.getText();
boolean booOne = box.isSelected();
boolean booTwo = str.compareToIgnoreCase(correctAnswer) == 0;
if (booOne && booTwo) {
score++;
showScore.setText("分数:" + score);
time.stop(); // 停止计时
maxTime = MAX;
readOneQuesion(); // 读入下一道题目
}
box.setSelected(false);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == time) {
showTime.setText("剩:" + maxTime + "秒");
maxTime--;
if (maxTime <= 0) {
maxTime = MAX;
readOneQuesion(); // 读入下一道题目
}
} else if (e.getSource() == reStart) {
setTestFile(testFile);
}
}
}
十五、泛型与集合框架
泛型类就是<>里面的类。
泛型(Generics)是 Java 语言中一种非常重要的特性,它允许在定义类、接口和方法时使用类型参数(type parameter)。类型参数在实例化或调用时会被具体的类型替换,从而为我们提供了一种编写可重用代码的方式。
泛型的一个重要应用就是在 Java 集合框架(Collections Framework)中。集合框架为我们提供了一组用于存储和操作对象集合的类和接口。这些类和接口都支持泛型,允许我们指定集合中元素的类型。
下面是一个简单的例子,演示了如何使用泛型和集合框架来创建一个列表并向其中添加元素:
import java.util.ArrayList;
import java.util.List;
public class GenericsExample {
public static void main(String[] args) {
// 创建一个列表,用于存储字符串
List<String> list = new ArrayList<>();
// 向列表中添加元素
list.add("apple");
list.add("banana");
list.add("cherry");
// 遍历列表并打印元素
for (String item : list) {
System.out.println(item);
}
}
}
在这个例子中,我们使用 `ArrayList` 类来创建一个列表,并使用泛型语法 `<String>` 来指定列表中元素的类型为 `String`。这样,在向列表中添加元素时,编译器就会检查我们添加的元素是否为 `String` 类型,从而保证了类型安全。
泛型类的使用方式:
一般格式:类<> 对象 = new 类<>(); //一般来说<>里面的类型就是泛型类型。
泛型类使用在链表中:
LinkedList<E> 泛型类。创建以链表结构存储数据。它创建的对象称为链表对象。
LinkedList<String> mylist = new LinkedList<String>();
迭代器方法:Iterator<String> iter = mylist.iterator(); mylist是一个泛型字符串。
它可以调用iter.hasNext()方法遍历mylist字符串。
泛型最大的优点就是不需要强行转换数据类型。而一般的链表,从链表提取get数据,都需要强制转换为String类型。定义泛型之后就不需要了。参考书籍P447.
排序链表:
public static sort(List<E> list) //按升序排列
public binarySearch(List<T> list,T key,CompareTo<T> c) //二分法的查找
堆栈:(后进先出,只在一端操作)
总是在顶端进行操作,最顶端的位置是1。Stack<>创建堆栈:
Stack<Interger> stack = new Stack<Interger>();
push()压栈:stack.push(new Integer(1));
pop()弹栈:Interger F1 = stack.pop()
empty()检查是否还有数据:
peek()获取顶端的数据,但不删除:
散列映射:
HashMap<K,V>泛型类创建的对象称作散列映射(也就是键值对)。
HashMap<String,Student> hashtable = HashMap<String,Student>();
hashtable是一个键值对数据。
例题:
英汉小字典GUI设计。
树集:
TreeSet<E>泛型类,它创建的对象叫做树集。
TreeSet<String> mytree = new TreeSet<String>();
把一个元素插入树集比插入数组或链表的效率更高。
树映射:
TreeMap<K,V>类,它的对象称为树映射。
参考书籍P457
应用举例:
使用对象流实现商品库存的录入与显示系统。