当多个线程之间,访问了共享的数据,就会产生线程安全问题
我们以一个售票厅的例子举例说明:
会发现:
所以,如果数据共享 ,但线程中只进行读 ,是不会存在安全问题的。
CPU
的控制权时,其它线程只能等待其执行完同一时间,只能执行一件事,就叫同步
同步有三种方式:
同步代码块 里放可能会产生线程安全的代码(访问了共享数据的代码),表示只对这个区块的资源进行互斥访问
synchronized(同步锁){
// 需要同步操作的代码
}
注意:
使用了一个锁对象 ,这个锁对象叫同步锁 ,也叫对象锁、对象监视器
t0
抢到了CPU
的执行权,执行run()
方法,遇到synchronized
代码块t1
抢到了CPU
的执行权,执行run()
方法,遇到synchronized
代码块t1
进入阻塞状态,直到t0
归还锁对象(t0
要执行完同步代码块中内容,会把锁对象归还给同步代码块 )总结: 同步中的线程,没有执行完,不会释放锁。同步外的线程,没有锁,进不去同步。
同步保证了,只有一个线程在同步中执行共享数据,保证了安全
程序频繁判断锁、获取锁、释放锁 ,程序的效率会降低
把同步代码块中的内容,放在同步方法中,调用该方法即可
修饰符 synchronized 返回值类型 函数名() {}
例: public synchronized void method() {}
同步方法使用的锁对象,就是Runnable
的实现类对象,就是这个:
在同步方法前加static
修饰,访问的共享数据也用static
修饰
// 访问的必须是静态共享
修饰符 static synchronized 返回值类型 函数名() {}
例: public static synchronized void method() {}
静态同步方法使用的锁对象,是本类的class属性
–>class文件对象(反射)
(之所以不用this
, 因为this
是创建对象时产生的。静态方法的创建优先于对象创建。)
JDK1.5
后有一个新的解决线程安全问题的方法,叫做Lock锁
。
Lock
接口,比 synchronized
更加好用
void lock()获取锁
和void unlock()释放锁
ReentrankLock
步骤:
ReentrankLock
对象提示: 最好把代码放在try...catch
中,并且把释放锁 放在finally
中,这样无论有没有异常,都会释放锁。
Thread
中有一个嵌套类(内部类)Thread.State
,记录着线程的状态
下面,以线程通信案例 来进行说明
wait();
方法,进入无限等待状态notify();
,退出无限等待状态注意事项:
wait()
和notify()
(这两个都是Object类
中的方法)两个方法:
注意: obj.wait()
如果不加参数,就是无限等待。但它可以接收一个毫秒值做参数,等到时间走完,还没有得到唤醒,会自动醒来 ,此时相当于Thread.sleep()
方法
notify()
如果有多个等待线程,随机唤醒一个
notifyAll()
如果有多个等待线程,全部唤醒
可以发现,一次只唤醒一个线程(随机的)
把notify()
变为notifyall()
会得到如下结果
线程间通讯: 多个线程在处理同一资源,但线程的任务不同
多个线程操作同一份数据,就会发生数据的抢夺,通过等待唤醒机制 可以避免这种争夺发生
注意: 被通知(notify)后,只是进入了可运行 状态,不是立即执行的(因为还在同步代码块那,还要抢锁)
分析:
两个线程的状态是通信(互斥)状态,必须保证两个线程只有一个在执行
注:
线程池: 容纳多个线程的容器,其中的线程可以复用,无需反复创建线程而消耗过多的资源
线程池是一个容器 ,也即一个集合(LinkedList
)
步骤:
Thread t = List.remove(index)
、Thread t = LinkedList.removeFirst()
(线程只能被一个任务使用)List.add(t)
、LinkedList.addLast(t)
JDK1.5
后,JDK
内置了线程池
,可以直接使用。
线程池的好处:
java.util.concurrent.Executors
是线程池的工厂类,用来生成线程池
static ExecutorService newFixedThreadPool(int 线程数目)
参数:线程数目
返回值:ExecutorService接口的实现类对象(可以使用接口类型来接收,这叫多态。这是一种【面向接口编程】)
java.util.concurrent.ExecutorService
线程池接口
// 用来从线程池中获取线程,调用start方法,执行线程任
submit(Runnable task) // 提交一个Runnable任务用于执行
// 销毁线程池(不建议执行)
void shutdown()
函数式编程思想: 强调做什么,而不是以某种形式做(面向对象强调通过对象来做)
run()
体内的代码让Thread()
知晓。把怎么做 转向做什么 的本质上,而不在乎过程与实质 ,我们只是为了一个结果 。JDK1.8
中加入了Lambda
表达式匿名内部类的好处是省去了实现类的定义 , 缺点是语法复杂
// Lambda的标准格式
(参数类型 参数名称) -> {代码体}
() : 接口中抽象方法的参数列表
-> : 传递的意思
{} : 重写接口的参数方法
凡是可根据上下文推导的,都可以省略
一些案例:
注: 只有一个抽象方法的接口叫函数式接口