文章目录
今日重点:
(1)掌握创建多线程的4种方式
(2)掌握线程通信中的方法
(3)完成生产者消费者的案例
(4)掌握String的定义,特性和常用方法
(5)理解String实例化时,内存结构的变化
(6)知晓什么是死锁,死锁产生的原因和解决办法
(7)知晓如何保证单例模式中的懒汉式的线程安全,要求掌握代码编写
一,复习
- 进程与线程的区别
- 并发与并行
- 传统创建线程的两种方式
- 继承Thread类
- 实现Runnable接口
- 线程常用方法,优先级的使用
- 线程的声明周期,:Thread.State的内部枚举类
- 如何解决线程同步的安全问题
- 同步机制
- 同步代码块与同步方法
- 同步监视器:任何一个类的对象都可以充当。但是要求多个线程必须是同一个监视器
- 共享数据:多个线程共同操作的变量
- 操作共享数据的代码:不能够包多,也不能包少
- lock锁
- 线程同步的好处:解决了线程的安全问题,体现了数据的一致性
- 线程同步以后的弊端:并发性降低,执行效率低
二,基于同步机制的延展问题
1.死锁
线程的同步机制带来的问题:死锁
1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
3. 我们在编程程序时,要避免出现死锁!!
/**
* 本题的分析,s1在等s2,s2在等s1从而造成死锁
*
*
* @author
*
*/
public class DeadLockTest {
public static void main(String[] args) {
StringBuilder s1 = new StringBuilder();
StringBuilder s2 = new StringBuilder();
new Thread() {
public void run() {
synchronized (s1) {
s1.append("a");
s2.append("1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
System.out.println(111);
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (s2) {
s1.append("c");
s2.append("3");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
}
}
2.解决懒汉式线程安全问题
//线程 操作 资源类
public class SingletonTest {
public static void main(String[] args) {
Customer customer = new Customer();
Thread t1 = new Thread(customer);
Thread t2 = new Thread(customer);
Thread t3 = new Thread(customer);
System.out.println("666");
t1.start();
t2.start();
t3.start();
}
}
class bank {
private bank() {
}
private static bank bank = null;
public static bank getInstance() {
if (bank == null) {
synchronized (Thread.class) {
if (bank == null) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
bank = new bank();
}
}
}
return bank;
}
}
/*
对bank中的单例模式进行测试
*/
class Customer implements Runnable {
private bank bank;
@Override
public void run() {
bank = bank.getInstance();
}
}
三,JDK5.0之后采用lock锁解决线程同步问题
除了使用synchronized之外还可以使用lock来对线程同步问题进行解决
步骤:
1. 创建Lock实现类的对象:ReentrantLock (可重入锁) --->保证多个线程使用同一个Lock对象
2. 调用Lock的方法:lock()。调用此方法的线程就可以操作共享数据。其他线程等待
3. 在操作共享数据时,可以调用Lock的方法:unlock()。此方法一定要执行!
二、面试题:synchronized 和 Lock方式的异同?
相同点:解决线程的安全问题的两种不同方式。 synchronized在jdk1.0就有。 Lock在jdk5.0新增
不同点:synchronized是自动结束的,但是lock是需要调用unlock方法解决的
在线程通信过程中,lock加上condition更加灵活
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
Windows windows = new Windows();
Thread t1 = new Thread(windows);
Thread t2 = new Thread(windows);
Thread t3 = new Thread(windows);
t1.start();
t2.start();
t3.start();
}
}
class Windows implements Runnable {
private int ticket = 100;
private ReentrantLock Lock = new ReentrantLock();
@Override
public void run() {
while (true) {
Lock.lock();
try {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(ticket > 0) {
System.out.println(Thread.currentThread().getName()+",售票:"+ticket+"号");
ticket--;
}else {
break;
}
} finally {
Lock.unlock();
}
}
}
}
四,线程的通信
1.线程通信基本常识
1.线程通信涉及到的三个方法:
wait():在同步结构中执行此方法会释放同步监视器
notify():唤醒被wait的线程,如果有多个被wait的线程,则唤醒优先级高的线程
notifyAll():唤醒所有被wait的线程
2.说明:
①三个方法必须使用在同步代码块或者同步方法中,不能在lock中。
②三个方法的调用者必须是同步监视器,否则就会报:java.lang.IllegalMonitorStateException
③三个方法定义在java.lang.Object中(因为任意对象都可以充当同步监视器)
public class CommunicationTest {
public static void main(String[] args) {
PrintNum printNum = new PrintNum();
Thread t1 = new Thread(printNum);
Thread t2 = new Thread(printNum);
t1.start();
t2.start();
}
}
class PrintNum implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (num < 100) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":"
+ num);
num++;
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
2.思考
思考:lock中的condition操作
例如1:有三个线程 需要轮流打印 唤醒的时候怎么控制唤醒的就是某一个呢?
例如2:有两个线程 轮流打印两个信息 怎么实现呢?
3.面试题
wait和sleep有什么区别
相同点:
一旦执行,都能够导致当前线程阻塞
不同点:
第一点:sleep在同步结构中,执行此方法不会释放同步监视器
wait在同步结构中,执行此方法一定会释放同步监视器
第二点:wait()定义的位置有限制,sleep可以在任意需要使用的的位置被调用,
第三点:sleep是Thread中定义,wait:Object中定义
第四点:结束等待的方式不同:sleep等待时间结束后就会自动结束,wait是等待notify唤醒
4.补充
释放锁的操作
当前线程的同步方法,同步代码块执行结束
当前线程在同步代码块,同步方法中遇到break,return终止了该代码块,该方法继续执行。
当前线程在同步代码块,同步方法中出现了未处理的Error或者Exception,导致异常结束
当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁
不会释放锁的操作
线程执行同步代码块或者同步方法时,程序调用Thread.sleep(),Thread.yield()方法暂停当前线程的执行
线程执行同步代码块的时候,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
尽量避免使用suspend()和resume()来控制线程
5.生产者和消费者问题
/**
* 线程 操作 资源类
*
* @author
*
*/
public class ConsumerProducerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor p1 = new Productor(clerk);
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
Thread t1 = new Thread(p1,"生产者1");
Thread t2 = new Thread(c1,"消费者1");
Thread t3 = new Thread(c2,"消费者2");
t1.start();
t2.start();
t3.start();
}
}
class Productor implements Runnable {
Clerk Clerk = new Clerk();
public Productor(Clerk clerk) {
super();
Clerk = clerk;
}
@Override
public void run() {
System.out.println("=========生产者开始生产产品========");
while (true) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Clerk.addProduct();
}
}
}
class Consumer implements Runnable {
Clerk Clerk = new Clerk();
public Consumer(Clerk clerk) {
super();
Clerk = clerk;
}
@Override
public void run() {
System.out.println("=========消费者开始消费产品========");
while (true) {
try {
Thread.sleep(80);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Clerk.minusProduct();
}
}
}
class Clerk {
private int productNum = 0;
public synchronized void addProduct() {
if (productNum < 20) {
productNum++;
System.out.println(Thread.currentThread().getName() + "生产了第"
+ productNum + "个产品");
// 唤醒消费者
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void minusProduct() {
if (productNum > 0) {
System.out.println(Thread.currentThread().getName() + "消费了第"
+ productNum + "个产品");
productNum--;
// 唤醒生产者
this.notifyAll();
} else {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
五,JDK5.0之后新增了两种创建线程的方式
1.实现Callable()接口
步骤
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值
get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
2.使用线程池
//创建并使用多线程的第四种方法:使用线程池
//使用线程池创建多线程的好处:
//1.降低了资源的消耗,使用完的线程可以被复用。
//2.提高了程序的响应速度。
//3.便于管理。
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// //设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
service1.setMaximumPoolSize(20);
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
六,字符串的使用
1.String的介绍
1. public final class String implements java.io.Serializable, Comparable
> 不可被继承
> Serializable:标识接口,实现序列化机制的接口
> Comparable:实现对象比较大小的接口
> String是类,属于引用类型变量。
2.String的不可变性
3.String实例化的情况
/*
* String的实例化的情况
*
* 两种方式:
* 方式一:使用字面量的方式
* 方式二:使用new的方式
*
* 面试题:使用new的方式创建一个字符串,内存中创建了几个对象? 堆空间中创建的对象、字符串常量池中。
*/
4.String的连接操作
/*
* String的连接操作
*
* 1. 连接运算中,如果使用的都是字面量,则在常量池中声明此字符串或使用现有的字符串。
* 2. 连接运算中,如果使用的是变量,则需要在堆中重新开辟空间,保存此字符串的值。
* 3. 通过字符串调用intern(),返回此字符串在字符串常量池中的字面量。
*/
5.String中的常用方法
/*
* int length():返回字符串的长度: return value.length
* char charAt(int index): 返回某索引处的字符return value[index]
* boolean isEmpty():判断是否是空字符串:return value.length == 0
* String toLowerCase():使用默认语言环境,将 String 中的所有字符转换为小写
* String toUpperCase():使用默认语言环境,将 String 中的所有字符转换为大写
* String trim():返回字符串的副本,忽略前导空白和尾部空白
* boolean equals(Object obj):比较字符串的内容是否相同
* boolean equalsIgnoreCase(String anotherString):与equals方法类似,忽略大小写
* String concat(String str):将指定字符串连接到此字符串的结尾。 等价于用“+”
* int compareTo(String anotherString):比较两个字符串的大小
* String substring(int beginIndex):返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
* String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。
*
*
*
*
*/
/*
* boolean endsWith(String suffix):测试此字符串是否以指定的后缀结束
* boolean startsWith(String prefix):测试此字符串是否以指定的前缀开始
* boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
* boolean contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true
* int indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引
* int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
* int lastIndexOf(String str):返回指定子字符串在此字符串中最右边出现处的索引
* int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索
* 注:indexOf和lastIndexOf方法如果未找到都是返回-1
*
* String replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
* String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
*/